├── .gitignore ├── .npmignore ├── .travis.yml ├── HISTORY.md ├── README.md ├── bin ├── cli.js ├── consts.js ├── help.txt ├── invoker.js ├── master.js ├── starter.js └── worker.js ├── demo ├── app.js └── cizefile.js ├── design ├── icon.afphoto ├── icon.png ├── logo.afphoto ├── logo.jpg └── logo.png ├── index.js ├── lib ├── by.js ├── context.js ├── cron.js ├── job.js ├── parallel.js ├── project.js ├── series.js ├── server.js ├── shell.js ├── store.js └── utils.js ├── package.json ├── screenshot └── monitor.png ├── test ├── by.test.js ├── context.test.js ├── index.test.js ├── job.test.js ├── parallel.test.js ├── project.test.js ├── series.test.js ├── server.test.js └── shell.test.js └── web ├── app.js ├── common └── utils.js ├── config.development.yaml ├── config.production.yaml ├── config.yaml ├── controllers ├── api.js ├── auth.js └── main.js ├── filters └── auth.js ├── global.js ├── locales ├── en.json ├── zh-hk.json ├── zh-sg.json ├── zh-tw.json └── zh.json ├── models └── .gitkeep ├── package.json ├── public ├── css │ └── common.css ├── favicons │ ├── android-chrome-144x144.png │ ├── android-chrome-192x192.png │ ├── android-chrome-36x36.png │ ├── android-chrome-48x48.png │ ├── android-chrome-72x72.png │ ├── android-chrome-96x96.png │ ├── apple-touch-icon-114x114.png │ ├── apple-touch-icon-120x120.png │ ├── apple-touch-icon-144x144.png │ ├── apple-touch-icon-152x152.png │ ├── apple-touch-icon-180x180.png │ ├── apple-touch-icon-57x57.png │ ├── apple-touch-icon-60x60.png │ ├── apple-touch-icon-72x72.png │ ├── apple-touch-icon-76x76.png │ ├── apple-touch-icon-precomposed.png │ ├── apple-touch-icon.png │ ├── browserconfig.xml │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon-96x96.png │ ├── favicon.ico │ ├── manifest.json │ ├── mstile-144x144.png │ ├── mstile-150x150.png │ ├── mstile-310x150.png │ ├── mstile-310x310.png │ ├── mstile-70x70.png │ └── safari-pinned-tab.svg ├── images │ └── logo.png └── js │ └── main.js └── views ├── alert.html ├── auth.html ├── console.html ├── main.html ├── master.html ├── record.html ├── setting.html └── trigger.html /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .vscode 3 | jsconfig.json 4 | node_modules/ 5 | coverage/ 6 | demo/data 7 | demo/works 8 | test/workspace -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .vscode 3 | jsconfig.json 4 | node_modules/ 5 | coverage/ 6 | demo/data 7 | demo/works 8 | screenshot/ 9 | test/workspace -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | env: 2 | - CXX=g++-4.8 3 | addons: 4 | apt: 5 | sources: 6 | - ubuntu-toolchain-r-test 7 | packages: 8 | - g++-4.8 9 | language: node_js 10 | node_js: 11 | - "4" -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | ### 0.3.2 2 | 1. 重写 CLI 逻辑 3 | 2. 支持强制在「独立进程」中执行 Job 4 | 3. 增加用 CLI 执行 Job 5 | 6 | ### 0.1.8 7 | 1. Job.beforeRun 支持异步方式 8 | 2. 修复清理后的标准输出合并 Bug 9 | 10 | ### 0.1.6 11 | 1. 改进父子 Job 标准输入合并 12 | 2. 改进内置扩展 cize.by/cize.series/cize.parallel 13 | 3. 改进针对 cizefile 的 watch 和 reload 14 | 4. 添加 Job.afterRun 15 | 5. 手动触发参数支持 YAML 格式 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # CIZE 是什么? 3 | CIZE 是一个「持续集成」工具,希望能让开发人员更快捷的搭建一个完整、可靠、便捷的 CI 服务。 4 | 甚至可以像 Gulp 或 Grunt 一样,仅仅通过一个 ```cizefile.js``` 即可完成几乎所有的工作。 5 | 6 | [![npm version](https://badge.fury.io/js/cize.svg)](http://badge.fury.io/js/cize) 7 | [![Build Status](https://travis-ci.org/Houfeng/cize.svg?branch=master)](https://travis-ci.org/Houfeng/cize) 8 | 9 | 10 | 11 | # 快速搭建 12 | ### 全局安装 13 | ```sh 14 | $ [sudo] npm install cize -g 15 | ``` 16 | 17 | ### 编写 Job 18 | 19 | 新建 cizefile.js 20 | ``` 21 | $ mkdir your_path 22 | $ cd your_path 23 | $ vim cizefile.js 24 | ``` 25 | 26 | 输入如下内容 27 | ```js 28 | //定义「项目」 29 | const demo = cize.project('demo', {}); 30 | 31 | //定义一个 Job,这是一个最基础的 Job 32 | demo.job('hello', function (self) { 33 | self.console.log('hello world'); 34 | self.done(); 35 | }); 36 | ``` 37 | 38 | 然后,在「工作目录」中执行 ```cize``` 启动服务 39 | 40 | ``` 41 | $ cize 42 | Strarting... 43 | The server on "localhost:9000" started 44 | ``` 45 | 默认会启动和 CPU 核数相同的「工作进程」。 46 | 47 | 接下来,可以在浏览器中访问 ```http://localhost:9000``` , 48 | 并可以在 UI 中手动触发这个名为 ```hello``` 的 Job 49 | 50 | # 定义 Project 51 | ```js 52 | const demo = cize.project('demo', { 53 | ... 54 | //可以在此添加针对项目的配置 55 | ... 56 | }); 57 | ``` 58 | 注意,即便一个项目不需要任何配置,也不能省略第二个参数, 59 | 没有第二个参数时 ```cize.project(name)``` 为获取指定的项目 60 | 61 | # 定义 Job 62 | 假定现在已经有一个定义好的名为 ```demo``` 的 ```project``` 63 | 64 | ### 用 js 编写的 Job 65 | ```js 66 | demo.job('test', function (self) { 67 | self.console.log('test'); 68 | self.done(); 69 | }); 70 | ``` 71 | 这是最基础的 Job 类型,是其它 Job 类型或「扩展」的基础。 72 | 73 | ### 用 shell 编写的 Job 74 | ```js 75 | demo.job('test', cize.shell(function () { 76 | /* 77 | echo "hello world" 78 | */ 79 | })); 80 | ``` 81 | 定义一个用 SHELL 编写的 Job,用到了 cize.shell,这是一个「内置扩展」 82 | 83 | ### 定时执行的 Job 84 | ```js 85 | demo.job('test', cize.cron('* */2 * * * *', cize.shell(function () { 86 | /* 87 | echo "hello world" 88 | */ 89 | }))); 90 | ``` 91 | 如上定义了一个每两分种触发一次的 Job 并且,嵌套使用了 shell. 92 | 93 | ### 监听其它 Job 的 Job 94 | ```js 95 | demo.job('test2', cize.by('test1', function(self){ 96 | self.console.log('hello'); 97 | self.done(); 98 | }); 99 | ``` 100 | 如下,在 test1 执行成功后,将会触发 test2 101 | 102 | ### 串行执行的 Job 103 | ```js 104 | demo.job('test', cize.series([ 105 | "test1", 106 | function(self){ 107 | self.console.log('hello'); 108 | self.done(); 109 | }, 110 | "test3" 111 | ])); 112 | ``` 113 | series 是一个内置扩展,可以定义一个「串行执行」多个步骤的任务列表,每个步骤可以是一个任意类型的 job, 114 | 也可以是指定要调用的其它 Job 的名称。 115 | 116 | ### 并行执行的 Job 117 | ```js 118 | demo.job('test', cize.parallel([ 119 | "test1", 120 | function(self){ 121 | self.console.log('hello'); 122 | self.done(); 123 | }, 124 | "test3" 125 | ])); 126 | ``` 127 | series 是一个内置扩展,可以定义一个「并行执行」多个步骤的任务列表,每个步骤可以是一个任意类型的 job, 128 | 也可以是指定要调用的其它 Job 的名称。 129 | 130 | ### 多步嵌套的 Job 131 | CIZE 所有的 Job 可以自由嵌套,例如: 132 | ```js 133 | demo.job('test', cize.parallel([ 134 | "test1", 135 | function(self){ 136 | self.console.log('hello'); 137 | self.done(); 138 | }, 139 | "test3", 140 | cize.series([ 141 | "test4", 142 | cize.shell(function(){ 143 | /* 144 | echo hello 145 | */ 146 | }) 147 | ]) 148 | ])); 149 | ``` 150 | 当你使用一个「外部扩展」时,也可以混合使用。 151 | 152 | # 编写一个扩展 153 | 如上用到的 cize.shell、cize.series、cize。parallel、cize.cron、cize.by 是 cize 默契认包含的「内置扩展」。 154 | 编写一个「外部扩展」和「内置扩展」并无本质区别,如下: 155 | ```js 156 | module.exports = function(options...){ 157 | return function(self){ 158 | //处理逻辑 159 | }; 160 | }; 161 | ``` 162 | 如查需要在 Job 定义时进行一些处理,可以使用 ```register``` ,如下 163 | ```js 164 | module.exports = function(options...){ 165 | return { 166 | register: function(Job){ 167 | //Job 是你的「自定义 Job 类型」 168 | //注册时逻辑 169 | }, 170 | runable: function(self){ 171 | //执行时逻辑 172 | } 173 | }; 174 | }; 175 | ``` 176 | 可以将扩展发布为一个「npm 包」,让更多的人使用。 177 | 178 | # 服务选项 179 | 180 | 可以通过一些选择去控制 CI 服务的端口、密钥等,有两种方式,如下 181 | 182 | ### 在 cizefile.js 中配置 183 | ```js 184 | cize.config({ 185 | port: 9000, 186 | secret: '12345' 187 | }); 188 | ``` 189 | 190 | ### 通过命令行工具 191 | ```js 192 | cize ./ -p=port -s=secret 193 | ``` 194 | 通过 cize -h 可以查看完整的说明 195 | ```sh 196 | Usage: 197 | cize [folder|file] [options] 198 | 199 | Options: 200 | -w set the number of workers 201 | -p set the port 202 | -s set the secret 203 | -h display help information 204 | 205 | Example: 206 | cize ./ -p=9000 -s=12345 -w=4 207 | ``` 208 | 209 | # 更多内容 210 | 211 | 请访问 wiki: [https://github.com/Houfeng/cize/wiki](https://github.com/Houfeng/cize/wiki) 212 | 213 | # 路线图 214 | - 所有 Job 都在单儿独立在一个进程中执行(现在可能会有 n 个 job 共用一个主进程) 215 | - 集成 Docker -------------------------------------------------------------------------------- /bin/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const path = require('path'); 4 | const CmdLine = require('cmdline'); 5 | const cluster = require('cluster'); 6 | const pkg = require('../package.json'); 7 | const os = require('os'); 8 | const fs = require('fs'); 9 | const console = require('console3'); 10 | const stp = require('stp'); 11 | const cmdline = require('cmdline'); 12 | 13 | cmdline.NAME = pkg.name; 14 | cmdline.VERSION = pkg.version; 15 | cmdline.CONFIG_FILE_NAME = `${pkg.name}file.js`; 16 | cmdline.CONFIG_FILE_REGEXP = /\.js$/i; 17 | 18 | if (cluster.isMaster) { 19 | cmdline.HELP_INFO = stp(fs.readFileSync(path.normalize(`${__dirname}/help.txt`)).toString(), { 20 | cmd: cmdline.NAME, 21 | conf: cmdline.CONFIG_FILE_NAME 22 | }) + os.EOL; 23 | } 24 | 25 | cmdline 26 | .error(function (err) { 27 | console.error(err.message); 28 | }) 29 | .version(`${cmdline.NAME.toUpperCase()} ${cmdline.VERSION}`) 30 | .help(cmdline.HELP_INFO) 31 | .option(['-p', '--port'], 'number') 32 | .option(['-s', '--secret'], 'string') 33 | .option(['-w', '--worker'], 'number') 34 | .option(['-m', '--mode'], 'string') 35 | .option('--project', 'string') 36 | .option('--job', 'string') 37 | .option('--params', 'string*') 38 | .action({ options: ['--project', '--job'] }, function (project, job) { 39 | require('./invoker')(cmdline); 40 | return false; 41 | }) 42 | .action(function ($1) { 43 | //计算 confPath 44 | cmdline.configFile = path.resolve(process.cwd(), $1 || './'); 45 | if (!cmdline.CONFIG_FILE_REGEXP.test(cmdline.configFile)) { 46 | cmdline.configFile = path.normalize(`${cmdline.configFile}/${cmdline.CONFIG_FILE_NAME}`); 47 | } 48 | require(cluster.isMaster ? './master' : './worker')(cmdline); 49 | return false; 50 | }, false) 51 | .ready(); -------------------------------------------------------------------------------- /bin/consts.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | WORKER_START_DELAY: 500, 3 | WORKER_RLOAD_CODE: 1 4 | }; -------------------------------------------------------------------------------- /bin/help.txt: -------------------------------------------------------------------------------- 1 | Usage: 2 | ${cmd} [folder|file] [options] 3 | 4 | Options: 5 | -v, --version display version information 6 | -h, --help display help information 7 | -p, --port set the port 8 | -s, --secret set the secret 9 | -w, --worker set the number of workers 10 | -m, --mode run mode, in-process or new-process or docker 11 | --project project name 12 | --job job name 13 | --params invoke params 14 | 15 | Examples: 16 | ${cmd} ./ -p 9000 -s 12345 -w 4 17 | ${cmd} ./ -project demo --job test --params '{"name":"test"}' -------------------------------------------------------------------------------- /bin/invoker.js: -------------------------------------------------------------------------------- 1 | const starter = require('./starter'); 2 | const console = require('console3'); 3 | 4 | const EXIT_DELAY = 1000; 5 | 6 | module.exports = function (cmdline) { 7 | 8 | //初始化启动器 9 | starter.init(cmdline); 10 | 11 | //当发生错误时退出 12 | function exitWhenError(err) { 13 | console.error(err); 14 | process.exit(1); 15 | } 16 | 17 | //禁止 webServer 18 | starter.server.config({ 19 | webServer: false 20 | }); 21 | 22 | //重用 worker 逻辑 23 | starter.start(function (err) { 24 | if (err) return exitWhenError(err); 25 | starter.server.invoke( 26 | cmdline.options.project, 27 | cmdline.options.job, 28 | cmdline.options.params, 29 | //执行完成时 30 | function (err) { 31 | if (err) return exitWhenError(err); 32 | console.log('Execute is successful'); 33 | setTimeout(function () { 34 | process.exit(0); 35 | }, EXIT_DELAY); 36 | }, 37 | //调用状态 38 | function (status) { 39 | if (!status) return exitWhenError('Invoke is failed'); 40 | console.log('Invoke is successful'); 41 | console.log('Running...'); 42 | }); 43 | }); 44 | 45 | }; -------------------------------------------------------------------------------- /bin/master.js: -------------------------------------------------------------------------------- 1 | const cluster = require('cluster'); 2 | const path = require('path'); 3 | const fs = require('fs'); 4 | const os = require('os'); 5 | const console = require('console3'); 6 | const chokidar = require('chokidar'); 7 | const consts = require('./consts'); 8 | 9 | module.exports = function (cmdline) { 10 | 11 | //显示帮助 12 | function help() { 13 | console.log(cmdline.HELP_INFO); 14 | return true; 15 | } 16 | 17 | //显示启动信息 18 | console.write('Strarting.'); 19 | 20 | //检查 cizefile 21 | if (!fs.existsSync(cmdline.configFile)) { 22 | console.error(`${os.EOL}Not found "${cmdline.configFile}"${os.EOL}`); 23 | return help() && process.exit(1); 24 | } 25 | 26 | //初始化 worker 数量及记数变量 27 | var workerMax = Number(cmdline.options.worker) || os.cpus().length; 28 | var workerCount = 0; 29 | var workerAllReady = false; 30 | 31 | //启动 workers 32 | function startWorkers(num) { 33 | cluster.fork(); 34 | if ((--num) <= 0) return; 35 | setTimeout(function () { 36 | startWorkers(num); 37 | }, consts.WORKER_START_DELAY); 38 | } 39 | startWorkers(workerMax); 40 | 41 | //当有 worker 断开并退出时 42 | cluster.on('disconnect', (worker) => { 43 | workerCount--; 44 | console.warn(`#${worker.process.pid} disconnected`); 45 | cluster.fork(); 46 | }); 47 | 48 | //当有 worker 启动成功时 49 | cluster.on('message', function (worker, data, handle) { 50 | if (arguments.length === 2) { 51 | handle = data; 52 | data = worker; 53 | worker = undefined; 54 | } 55 | //如果发生错误 56 | if (!data.status) { 57 | console.error(os.EOL + data.message + os.EOL); 58 | return workerAllReady || process.exit(1); 59 | } 60 | //如果是某一 worker 重启 61 | if (workerAllReady) { 62 | return console.info(`#${data.pid} started`); 63 | } 64 | //初始起启信息 65 | process.stdout.write('.'); 66 | workerCount++; 67 | if (workerCount >= workerMax) { 68 | console.log(os.EOL + data.message); 69 | workerAllReady = true; 70 | } 71 | }); 72 | 73 | //监控配置文件 74 | chokidar.watch(cmdline.configFile, { 75 | ignoreInitial: true 76 | }).on('all', function (event, path) { 77 | console.info(`"${cmdline.configFile}" changed`); 78 | for (var id in cluster.workers) { 79 | cluster.workers[id].send(consts.WORKER_RLOAD_CODE); 80 | } 81 | }); 82 | 83 | }; -------------------------------------------------------------------------------- /bin/starter.js: -------------------------------------------------------------------------------- 1 | const ci = require('../'); 2 | const utils = ci.utils; 3 | const moduleCache = require('module')._cache; 4 | const path = require('path'); 5 | 6 | //初始化 7 | exports.init = function (cmdline) { 8 | this.cmdline = cmdline; 9 | this.server = ci; 10 | }; 11 | 12 | //加载配置 13 | exports.loadConfig = function () { 14 | //清理缓存 15 | moduleCache[this.cmdline.configFile] = null; 16 | //加载 cizefile 配置 17 | var config = require(this.cmdline.configFile); 18 | if (utils.isFunction(config)) { 19 | config(this.server); 20 | } 21 | }; 22 | 23 | exports.start = function (callback) { 24 | 25 | //默认或 cli 配置 26 | this.server.config({ 27 | configFile: this.cmdline.configFile, 28 | workspace: path.dirname(this.cmdline.configFile), 29 | port: Number(this.cmdline.options.port), 30 | secret: this.cmdline.options.secret, 31 | mode: this.cmdline.options.mode 32 | }); 33 | 34 | //加载 cizefile 35 | this.loadConfig(); 36 | 37 | //在 worker 中启动服务 38 | this.server.start(callback); 39 | }; -------------------------------------------------------------------------------- /bin/worker.js: -------------------------------------------------------------------------------- 1 | const cluster = require('cluster'); 2 | const consts = require('./consts'); 3 | const console = require('console3'); 4 | const starter = require('./starter'); 5 | 6 | module.exports = function (cmdline) { 7 | 8 | //初始化启动器 9 | starter.init(cmdline); 10 | 11 | //发生异常时向 master 发消息 12 | process.on('uncaughtException', function (err) { 13 | process.send({ 14 | status: false, 15 | message: err.toString(), 16 | pid: process.pid, 17 | workerId: cluster.worker.id 18 | }); 19 | }); 20 | 21 | //处理 master 发来的消息 22 | cluster.worker.on('message', function (code) { 23 | if (code != consts.WORKER_RLOAD_CODE) return; 24 | starter.loadConfig(); 25 | console.info(`#${process.pid} to reload`); 26 | }); 27 | 28 | //在 worker 中启动服务 29 | starter.start(function (err, info) { 30 | process.send({ 31 | status: !err, 32 | message: err ? err.toString() : info, 33 | pid: process.pid, 34 | workerId: cluster.worker.id 35 | }); 36 | }); 37 | 38 | }; 39 | -------------------------------------------------------------------------------- /demo/app.js: -------------------------------------------------------------------------------- 1 | var ci = require('../'); 2 | 3 | require('./cizefile'); 4 | 5 | ci.config({ 6 | configFile: require.resolve('./cizefile') 7 | }) 8 | 9 | ci.start(); -------------------------------------------------------------------------------- /demo/cizefile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 配置服务,可以指定「端口」、「密钥」、「工作目录」等 3 | * port:端口,默认为 9000 4 | * secret:密且,默认没启用,启用后登录「WEB 版监视界面」需要提供 5 | * workspace:工作目录,默认为 cizefile.js 所在目录 6 | **/ 7 | cize.config({ 8 | secret: '12345', 9 | //mode: 'new-process' 10 | }); 11 | 12 | /** 13 | * 定义「项目」,项目用于为 JOB 分组,第二个参数为「项目配置」 14 | * {} 不能省略,省略时为获取指定名称的项目 15 | **/ 16 | const demo = cize.project('demo', {}); 17 | 18 | /** 19 | * 定义一个 JOB,这是一个最基本的 JOB, 20 | * 其它各类,都是在此基础之上的「扩展」 21 | **/ 22 | demo.job('job1', function (self) { 23 | self.console.log('hello job1'); 24 | self.done(); 25 | }); 26 | 27 | /** 28 | * 定义一个用 SHELL 编写的 JOB 29 | * 如下用到了 cize.shell,这是多个「内置扩展」中的一个 30 | **/ 31 | demo.job('job2', cize.shell(function () { 32 | /* 33 | echo "hello job2" 34 | */ 35 | })); -------------------------------------------------------------------------------- /design/icon.afphoto: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Houfeng/cize/2a2818d80dffd11d95ccca57fcc8052881207331/design/icon.afphoto -------------------------------------------------------------------------------- /design/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Houfeng/cize/2a2818d80dffd11d95ccca57fcc8052881207331/design/icon.png -------------------------------------------------------------------------------- /design/logo.afphoto: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Houfeng/cize/2a2818d80dffd11d95ccca57fcc8052881207331/design/logo.afphoto -------------------------------------------------------------------------------- /design/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Houfeng/cize/2a2818d80dffd11d95ccca57fcc8052881207331/design/logo.jpg -------------------------------------------------------------------------------- /design/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Houfeng/cize/2a2818d80dffd11d95ccca57fcc8052881207331/design/logo.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const pkg = require('./package.json'); 3 | const Server = require('./lib/server'); 4 | 5 | var exports = new Server(); 6 | exports.Server = Server; 7 | exports.Project = require('./lib/project'); 8 | exports.Job = require('./lib/job'); 9 | exports.shell = require('./lib/shell'); 10 | exports.by = require('./lib/by'); 11 | exports.series = require('./lib/series'); 12 | exports.serial = require('./lib/series'); 13 | exports.parallel = require('./lib/parallel'); 14 | exports.cron = require('./lib/cron'); 15 | exports.crontab = require('./lib/cron'); 16 | exports.Store = require('./lib/store'); 17 | exports.pkg = require('./package.json'); 18 | exports.utils = require('./lib/utils'); 19 | 20 | Error.prototype.toString = function () { 21 | return this.message + os.EOL + this.stack; 22 | }; 23 | 24 | global.__defineGetter__(pkg.name, function () { 25 | return exports; 26 | }); 27 | 28 | module.exports = exports; -------------------------------------------------------------------------------- /lib/by.js: -------------------------------------------------------------------------------- 1 | const utils = require('./utils'); 2 | const Job = require('./job'); 3 | 4 | /** 5 | * 在一个 job 完成后触发 6 | * @param {String} triggerJobName 源 job 7 | * @param {Function} runable 可以执行函数 8 | **/ 9 | module.exports = function (triggers, runable) { 10 | if (!triggers) { 11 | throw new Error('Invalid parameter: triggers'); 12 | } 13 | if (!runable) { 14 | throw new Error('Invalid parameter: runable'); 15 | } 16 | return { 17 | register: function (NewJob) { 18 | var project = NewJob.project; 19 | if (utils.isString(triggers)) { 20 | triggers = triggers.split(','); 21 | } 22 | if (!utils.isArray(triggers)) { 23 | throw new Error('Invalid trigger'); 24 | } 25 | triggers.forEach(function (trigger) { 26 | if (trigger.indexOf('.') > -1) { 27 | //在新上下文调执行 28 | project.server.on('job.end:' + trigger, function (job) { 29 | project.invoke(NewJob, { 30 | root: true, 31 | target: job, 32 | params: job.params 33 | }); 34 | }); 35 | } else { 36 | //在同上下文件执行 37 | project.on('job.end:' + trigger, function (job) { 38 | job.invoke(NewJob, { 39 | target: job, 40 | root: true 41 | }); 42 | }); 43 | } 44 | }); 45 | }, 46 | runable: runable 47 | }; 48 | }; -------------------------------------------------------------------------------- /lib/context.js: -------------------------------------------------------------------------------- 1 | const Class = require('cify').Class; 2 | const EventEmitter = require('events'); 3 | const path = require('path'); 4 | const mkdir = require('mkdir-p'); 5 | const utils = require('./utils'); 6 | const async = require('async'); 7 | const Job = require('./job'); 8 | const fs = require('fs'); 9 | const rmdir = require('rmdir'); 10 | 11 | /** 12 | * 执行上下文,每次执行一个 job 都必须产生一个 context 13 | * 同 project 的 job 调用(通过 context.invoke),不会产生新的 context 14 | **/ 15 | const Context = new Class({ 16 | 17 | /** 18 | * 继承 EventEmitter 19 | **/ 20 | $extends: EventEmitter, 21 | 22 | /** 23 | * Context 的构建函数 24 | * @param {Project} project 所属 project 25 | * @param {Object} params 执行参数 26 | **/ 27 | constructor: function (project, params, parent) { 28 | EventEmitter.call(this); 29 | if (!project) { 30 | throw new Error('Invalid parameter: project'); 31 | } 32 | this.server = project.server; 33 | this.project = project; 34 | this.parent = parent; 35 | this.params = params || {}; 36 | this.id = utils.id(); 37 | this.jobs = []; 38 | }, 39 | 40 | /** 41 | * 创建一个新的上下文 42 | * @param {String} projectName 项目名称 43 | * @param {Object} params 执行参数 44 | **/ 45 | create: function (projectName, params) { 46 | if (!utils.checkName(projectName)) { 47 | throw new Error('Invalid parameter: projectName'); 48 | } 49 | var foundProject = this.server.project(projectName); 50 | if (!foundProject) { 51 | throw new Error('Not found project:' + projectName); 52 | } 53 | return new Context(foundProject, params || this.params, this); 54 | }, 55 | 56 | /** 57 | * 获取最顶层的 parent 58 | **/ 59 | deepParent: function () { 60 | var _parent = this; 61 | while (_parent.parent) { 62 | _parent = _parent.parent; 63 | } 64 | return _parent; 65 | }, 66 | 67 | /** 68 | * 在 context 初始化完成后触发 69 | * 1) 生成唯一ID 70 | * 2) 创建执行目录 71 | **/ 72 | ready: function (callback) { 73 | callback = callback || utils.NOOP; 74 | var self = this; 75 | self.paths = self.server.getContextPaths(self.id); 76 | async.series([ 77 | function (next) { 78 | mkdir(self.paths.out, next) 79 | }, 80 | function (next) { 81 | mkdir(self.paths.cwd, next) 82 | }, 83 | function (next) { 84 | fs.writeFile(self.paths.params, JSON.stringify(self.params), next); 85 | } 86 | ], callback); 87 | }, 88 | 89 | /** 90 | * 清理 context 相关的记录和磁盘文件 91 | **/ 92 | clean: function (callback) { 93 | var self = this; 94 | async.parallel([ 95 | function (done) { 96 | rmdir(self.paths.root, done); 97 | }, 98 | function (done) { 99 | self.server.store.remove({ 100 | contextId: self.id 101 | }, done); 102 | } 103 | ], callback); 104 | }, 105 | 106 | /** 107 | * 在当前 context 上调用一个 job 108 | * @param {Job|String} job job 名称或 Job 类型 109 | * @param {Object} options 执行选项 110 | * @param {Function} callback 执行完成时的回调 111 | **/ 112 | invoke: function (job, options, callback) { 113 | var self = this; 114 | if (!callback && utils.isFunction(options)) { 115 | callback = options; 116 | options = null; 117 | } 118 | callback = callback || utils.NOOP; 119 | options = options || {}; 120 | var TheJob = null; 121 | if (utils.isString(job)) { 122 | TheJob = self.project.job(job); 123 | } else { 124 | TheJob = job; 125 | } 126 | if (!TheJob) { 127 | callback(new Error('Invalid Job')); 128 | return false; 129 | } 130 | if (TheJob.project != self.project) { 131 | callback(new Error('Can\'t cross project invoke in a context')); 132 | return false; 133 | } 134 | var theJob = new TheJob(self, options); 135 | return theJob._run(callback); 136 | }, 137 | 138 | /** 139 | * 是否存在失败的 job 140 | **/ 141 | hasFailed: function () { 142 | return this.jobs.some(function (job) { 143 | return job.status == Job.status.FAILED; 144 | }); 145 | }, 146 | 147 | /** 148 | * 所有错误 149 | **/ 150 | errors: function (index) { 151 | var _errors = this.jobs.filter(function (job) { 152 | return !!job.error; 153 | }).map(function (job) { 154 | return job.error; 155 | }); 156 | return _errors[index] || _errors; 157 | }, 158 | 159 | /** 160 | * 所有结果 161 | **/ 162 | results: function (index) { 163 | var _results = this.jobs.filter(function (job) { 164 | return !!job.result; 165 | }).map(function (job) { 166 | return job.result; 167 | }); 168 | return _results[index] || _results; 169 | }, 170 | 171 | /** 172 | * 深度检查是否存在失败的 job 173 | **/ 174 | deepHasFailed: function () { 175 | return this.parent 176 | ? this.parent.deepHasFailed() && this.hasFailed() 177 | : this.hasFailed(); 178 | } 179 | 180 | }); 181 | 182 | module.exports = Context; -------------------------------------------------------------------------------- /lib/cron.js: -------------------------------------------------------------------------------- 1 | const CronJob = require('cron').CronJob; 2 | const net = require('net'); 3 | const spawn = require('child_process').spawn; 4 | const utils = require('./utils'); 5 | 6 | const LOCAL_HOST = '127.0.0.1'; 7 | const CRON_SERVER_PORT = 30405; 8 | const DELAY_FOR_CREATE_CRON_SERVER = 1000; 9 | 10 | if (process.env._CRON_SERVER) { 11 | 12 | function handle(data) { 13 | data.tasks.forEach(function (task) { 14 | var cronJob = new CronJob(task.expr, function () { 15 | utils.request.post(`http://${LOCAL_HOST}:${data.port}/api/projects/${task.projectName}/jobs/${task.jobName}/trigger?${data.tokenKey}=${data.token}`); 16 | }); 17 | cronJob.start(); 18 | }); 19 | } 20 | 21 | var cronServer = net.createServer(function (socket) { 22 | socket.on('data', function (data) { 23 | if (cronServer.ready) return; 24 | handle(JSON.parse(data)); 25 | cronServer.ready = true; 26 | }); 27 | }); 28 | cronServer.listen(CRON_SERVER_PORT); 29 | 30 | } else { 31 | 32 | function createCronServer(server, callback) { 33 | var cronServerPorcess = spawn(process.argv[0], [__filename], { 34 | env: { 35 | _CRON_SERVER: true, 36 | } 37 | }); 38 | cronServerPorcess.on('exit', function (code) { 39 | //console.warn('Cron server exit with', code); 40 | connectCronServer(server); 41 | }); 42 | setTimeout(callback, DELAY_FOR_CREATE_CRON_SERVER); 43 | }; 44 | 45 | function getCronData(server) { 46 | var rs = Object.create(null); 47 | rs.tasks = []; 48 | utils.each(server.projects, function (name, project) { 49 | utils.each(project.allJobs, function (name, job) { 50 | if (!job._cron_expr) return; 51 | rs.tasks.push({ 52 | expr: job._cron_expr, 53 | projectName: job.project.name, 54 | jobName: job.name 55 | }); 56 | }); 57 | }); 58 | rs.port = server.webServer.options.port; 59 | rs.tokenKey = `${server.pkg.name}-token`; 60 | rs.token = server.createToken(-1); 61 | return rs; 62 | } 63 | 64 | function connectCronServer(server) { 65 | var cronClient = new net.Socket(); 66 | cronClient.on('error', function (err) { 67 | createCronServer(server, function () { 68 | connectCronServer(server); 69 | }); 70 | }); 71 | cronClient.connect(CRON_SERVER_PORT, LOCAL_HOST, function () { 72 | var data = getCronData(server); 73 | cronClient.write(JSON.stringify(data)); 74 | }); 75 | } 76 | 77 | function registerToServer(server) { 78 | server.on('ready', function () { 79 | connectCronServer(server); 80 | }); 81 | } 82 | 83 | var cronServerStarted = false; 84 | 85 | /** 86 | * 定时执行计划任务 87 | * @param {String} expr 时间匹配表达式 88 | * @param {String} runable 可以执行函数 89 | **/ 90 | module.exports = function (expr, runable) { 91 | return { 92 | register: function (NewJob) { 93 | var project = NewJob.project; 94 | NewJob._cron_expr = expr; 95 | if (cronServerStarted) return; 96 | cronServerStarted = true; 97 | registerToServer(project.server); 98 | }, 99 | runable: runable 100 | }; 101 | }; 102 | 103 | } -------------------------------------------------------------------------------- /lib/job.js: -------------------------------------------------------------------------------- 1 | const Class = require('cify').Class; 2 | const EventEmitter = require('events'); 3 | const utils = require('./utils'); 4 | const fs = require('fs'); 5 | const Console = require('console3').Console; 6 | const path = require('path'); 7 | const async = require('async'); 8 | 9 | const JOB_BEGIN_EVENT = 'job.begin'; 10 | const JOB_END_EVENT = 'job.end'; 11 | 12 | const Job = new Class({ 13 | 14 | /** 15 | * 继承 EventEmitter 16 | **/ 17 | $extends: EventEmitter, 18 | 19 | /** 20 | * Job 的构造函数 21 | * @runable {Function} 可以运行的 Function 22 | **/ 23 | constructor: function (context, options) { 24 | EventEmitter.call(this); 25 | if (!context) { 26 | throw new Error('Invalid parameter: context'); 27 | } 28 | options = options || {}; 29 | if (this.isAbstract) return; 30 | this.context = context; 31 | this.context.jobs.push(this); 32 | this.parent = options.parent; 33 | this.target = options.target; 34 | this.id = utils.id(); 35 | this.server = context.server; 36 | this.project = context.project; 37 | this.params = context.params; 38 | this.paths = context.paths; 39 | //确定是否继承 stdout 40 | this._inheritStdout = (this.isShadow() && 41 | this.parent && 42 | this.parent.project == this.project && 43 | this.parent.stdout && 44 | !this.parent.stdout._writableState.finished && 45 | options.stdout != 'new'); 46 | //创建 stdout 47 | if (this._inheritStdout) { 48 | this.outFile = this.parent.outFile; 49 | this.stdout = this.parent.stdout; 50 | } else { 51 | this.outFile = path.normalize(`${this.paths.out}/${this.project.name}.${this.name}.txt`); 52 | this.stdout = fs.createWriteStream(this.outFile); 53 | } 54 | this.console = new Console(this.stdout, this.stdout); 55 | this.refused = false; 56 | }, 57 | 58 | /** 59 | * 标识为抽象类 60 | **/ 61 | isAbstract: true, 62 | 63 | /** 64 | * 调用其它 Job 65 | * @param {String|Job} job Job 名称或类型 66 | * @param {Function} callback 完成时的回调 67 | **/ 68 | invoke: function (job, options, callback) { 69 | if (!callback && utils.isFunction(options)) { 70 | callback = options; 71 | options = null; 72 | } 73 | callback = callback || utils.NOOP; 74 | options = options || {}; 75 | if (!options.root) { 76 | options.parent = this; 77 | } 78 | if (!utils.isString(job)) { 79 | if (job.project == this.project) { 80 | return this.context.invoke(job, options, callback); 81 | } else { 82 | return job.project.invoke(job, options, callback); 83 | } 84 | } 85 | var jobInfo = job.split('.'); 86 | if (jobInfo.length > 1) { 87 | var childContext = this.context.create(jobInfo[0], options.params || this.params); 88 | return childContext.ready(function (err) { 89 | if (err) return callback(err); 90 | return childContext.invoke(jobInfo[1], options, callback); 91 | }); 92 | } else { 93 | return this.context.invoke(jobInfo[0], options, callback); 94 | } 95 | }, 96 | 97 | /** 98 | * 运行 Job,启动成功返回 true ,失败返回 false 99 | * 启动成功,并不表达执行能通过 100 | * @param {Function} callback 执行回完成时的回调 101 | **/ 102 | _run: function (callback) { 103 | var self = this; 104 | callback = callback || utils.NOOP; 105 | if (!self.name) { 106 | callback(new Error('Required property: name')); 107 | return false; 108 | } 109 | if (!self._runable) { 110 | callback(new Error('Required property: runable')); 111 | return false; 112 | } 113 | self.beginTime = Date.now(); 114 | self._checkBeforeRun(function (err) { 115 | //检查是允许继续执行 116 | self.refused = !!err; 117 | if (self.refused) { 118 | return self._end(err, null, callback); 119 | } 120 | //创建 done 方法 121 | self.done = function (err, result) { 122 | self._end(err, result, callback); 123 | }; 124 | //设置状并尝试执行 125 | self._setStatus(Job.status.RUNING, function (err) { 126 | if (err) return callback(err, self); 127 | self._triggerBeginEvent(); 128 | utils.try(function () { 129 | self._runable(self); 130 | }, function (err) { 131 | self._end(err, null, callback); 132 | }); 133 | }); 134 | //-- 135 | }); 136 | return true; 137 | }, 138 | 139 | /** 140 | * 结束运行 141 | **/ 142 | _end: function (err, result, callback) { 143 | if (this._ended) { 144 | if (err) console.error(err.toString()); 145 | return; 146 | } 147 | this._ended = true; 148 | callback = callback || utils.NOOP; 149 | this.error = err; 150 | this.result = result; 151 | this.endTime = Date.now(); 152 | if (!err) { 153 | this._setStatus(Job.status.PASSED); 154 | } else { 155 | this._setStatus(Job.status.FAILED); 156 | this.console.error(err); 157 | } 158 | if (!this.refused) { 159 | this._execAfterRun(); 160 | } 161 | //虽然 refused 但是还将出发 end 事件,因为无论是否执行,但它结束了 162 | this._triggerEndEvent(); 163 | //如果不是继承的父实例的 stdout, 则进行关闭 164 | if (!this.parent || this.parent.stdio != this.stdio) { 165 | this.stdout.end(); 166 | } 167 | return this._outAppendToParent(callback); 168 | }, 169 | 170 | /** 171 | * 合并输出到父 job 172 | **/ 173 | _outAppendToParent: function (callback) { 174 | var self = this; 175 | //如已继承 stdout 或者没有 parent,则不执行合并 176 | if (self._inheritStdout || 177 | !self.parent || 178 | self.parent.stdout._writableState.finished) { 179 | return callback(self.error, self); 180 | } 181 | fs.exists(self.outFile, function (exists) { 182 | if (!exists) return callback(self.error, self); 183 | fs.readFile(self.outFile, function (err, data) { 184 | try { 185 | self.parent.stdout.write((err || data).toString()); 186 | } catch (err) { } 187 | callback(self.error, self); 188 | }); 189 | }); 190 | }, 191 | 192 | /** 193 | * 触发执行开始事件 194 | **/ 195 | _triggerEndEvent: function () { 196 | var self = this; 197 | self.$class.emit(JOB_END_EVENT, self); 198 | self.project.emit(`${JOB_END_EVENT}:${self.name}`, self); 199 | self.project.emit(JOB_END_EVENT, self); 200 | self.server.emit(`${JOB_END_EVENT}:${self.project.name}.${self.name}`, self); 201 | self.server.emit(JOB_END_EVENT, self); 202 | }, 203 | 204 | /** 205 | * 触发执行结束事件 206 | **/ 207 | _triggerBeginEvent: function () { 208 | var self = this; 209 | self.$class.emit(JOB_BEGIN_EVENT, self); 210 | self.project.emit(`${JOB_BEGIN_EVENT}:${self.name}`, self); 211 | self.project.emit(JOB_BEGIN_EVENT, self); 212 | self.server.emit(`${JOB_BEGIN_EVENT}:${self.project.name}.${self.name}`, self); 213 | self.server.emit(JOB_BEGIN_EVENT, self); 214 | }, 215 | 216 | /** 217 | * 是否是一个虚拟 job 实例 218 | **/ 219 | isShadow: function () { 220 | return !(this.project.jobs[this.name]); 221 | }, 222 | 223 | /** 224 | * 设置实例状态 225 | **/ 226 | _setStatus: function (status, callback) { 227 | this.status = status; 228 | if (!this.isAbstract) { 229 | this.server.store.save(this, callback); 230 | } else if (callback) { 231 | callback(); 232 | } 233 | }, 234 | 235 | /** 236 | * 是否可以执行 237 | **/ 238 | _checkBeforeRun: function (callback) { 239 | var self = this; 240 | if (!self._beforeRunHooks || self._beforeRunHooks.length < 1) { 241 | return callback(); 242 | } 243 | async.parallel(self._beforeRunHooks.map(function (hook) { 244 | return function (done) { 245 | var refusedErr = Error('Refused to run'); 246 | var hookArgumentNames = utils.getFunctionArgumentNames(hook); 247 | if (hookArgumentNames.length > 1) { 248 | hook.call(self, self, function (_state) { 249 | done(_state === false ? refusedErr : null); 250 | }); 251 | } else { 252 | var _state = hook.call(self, self); 253 | done(_state === false ? refusedErr : null); 254 | } 255 | }; 256 | }), callback); 257 | }, 258 | 259 | /** 260 | * 执行 run 后 hook 261 | **/ 262 | _execAfterRun: function () { 263 | var self = this; 264 | if (!self._afterRunHooks) return; 265 | self._afterRunHooks.forEach(function (hook) { 266 | if (!utils.isFunction(hook)) return; 267 | hook.call(self, self); 268 | }); 269 | }, 270 | 271 | /** 272 | * 清理 job 相关的记录和磁盘文件 273 | **/ 274 | clean: function (callback) { 275 | var self = this; 276 | async.parallel([ 277 | function (done) { 278 | fs.unlink(self.outFile, done); 279 | }, 280 | function (done) { 281 | self.server.store.remove({ 282 | _id: self.id 283 | }, done); 284 | } 285 | ], callback); 286 | } 287 | 288 | }); 289 | 290 | /** 291 | * 是否是一个虚拟 Job 类型 292 | **/ 293 | Job.isShadow = function () { 294 | return !(this.project.jobs[this.name]); 295 | }; 296 | 297 | /** 298 | * Job 实例的状态 299 | **/ 300 | Job.status = { 301 | RUNING: 100, 302 | PASSED: 200, 303 | FAILED: 300 304 | }; 305 | 306 | /** 307 | * 获取 job 记录 308 | * @param {Number} limit 指定记录条数 309 | * @param {Number} skip 跳过指定的条数 310 | * @param {Function} callback 完成时的回调函数 311 | **/ 312 | Job.getRecords = function (limit, skip, callback) { 313 | limit = limit || 100; 314 | skip = skip || 0; 315 | this.server.store.find({ 316 | projectName: this.project.name, 317 | name: this.name, 318 | refused: false, 319 | isShadow: false 320 | }, { offset: skip }, limit, ['beginTime', 'Z'], callback); 321 | }; 322 | 323 | /** 324 | * 清理记录和磁盘文件 325 | **/ 326 | Job.clean = function (condition, callback) { 327 | condition = condition || {}; 328 | condition.projectName = this.project.name; 329 | condition.name = this.name; 330 | return this.server.clean(condition, callback); 331 | }; 332 | 333 | /** 334 | * 查找一个执行记录 335 | **/ 336 | Job.getRecordBySn = function (sn, callback) { 337 | this.server.store.findOne({ 338 | projectName: this.project.name, 339 | name: this.name, 340 | sn: sn 341 | }, callback); 342 | }; 343 | 344 | /** 345 | * 添加执行前 hook 346 | **/ 347 | Job.beforeRun = function (hook) { 348 | if (!utils.isFunction(hook)) { 349 | throw new Error('Invalid parameter: hook'); 350 | } 351 | var proto = this.prototype; 352 | proto._beforeRunHooks = proto._beforeRunHooks || []; 353 | proto._beforeRunHooks.push(hook); 354 | }; 355 | 356 | /** 357 | * 添加执行后 hook 358 | **/ 359 | Job.afterRun = function (hook) { 360 | if (!utils.isFunction(hook)) { 361 | throw new Error('Invalid parameter: hook'); 362 | } 363 | var proto = this.prototype; 364 | proto._afterRunHooks = proto._afterRunHooks || []; 365 | proto._afterRunHooks.push(hook); 366 | }; 367 | 368 | /** 369 | * 定义子 Job 370 | **/ 371 | Job.defineJob = function (name, runable) { 372 | return this.project.defineJob(name, runable, this); 373 | }; 374 | 375 | /** 376 | * 获取最顶层的 parent 377 | **/ 378 | Job.deepParent = function () { 379 | var _parent = this; 380 | while (_parent.parent) { 381 | _parent = _parent.parent; 382 | } 383 | return _parent; 384 | }; 385 | 386 | /** 387 | * 初始化需特殊处理的静态成员 388 | **/ 389 | Job._initSpecialDefine = function () { 390 | //添加处理处理能力开始 391 | this._eventEimter = this._eventEimter || new EventEmitter(); 392 | utils.each(this._eventEimter, function (name, method) { 393 | if (!utils.isFunction(method)) return; 394 | this[name] = function () { 395 | return this._eventEimter[name].apply(this._eventEimter, arguments); 396 | }; 397 | }.bind(this)); 398 | //添加处理处理能力结束 399 | }; 400 | 401 | module.exports = Job; -------------------------------------------------------------------------------- /lib/parallel.js: -------------------------------------------------------------------------------- 1 | const async = require('async'); 2 | const Job = require('./job'); 3 | const utils = require('./utils'); 4 | const fs = require('fs'); 5 | 6 | /** 7 | * 并行步骤的任务 8 | **/ 9 | module.exports = function (array) { 10 | if (!utils.isArray(array)) { 11 | array = [].slice.call(arguments); 12 | } 13 | var NewJob, project, ChildJobs; 14 | return { 15 | register: function (_NewJob) { 16 | NewJob = _NewJob; 17 | project = _NewJob.project; 18 | var index = 0; 19 | ChildJobs = array.map(function (runable) { 20 | return NewJob.defineJob(`${NewJob.name}-${++index}`, runable); 21 | }); 22 | }, 23 | runable: function (self) { 24 | async.parallel(ChildJobs.map(function (ChildJob) { 25 | return function (next) { 26 | self.invoke(ChildJob, { 27 | stdout: 'new' 28 | }, next); 29 | }; 30 | }), function (err, result) { 31 | if (self.context.hasFailed()) { 32 | err = new Error(`QueueFailed: ${err && err.message}`); 33 | } 34 | self.done(err, result); 35 | }); 36 | } 37 | } 38 | }; -------------------------------------------------------------------------------- /lib/project.js: -------------------------------------------------------------------------------- 1 | const Class = require('cify').Class; 2 | const Job = require('./job'); 3 | const EventEmitter = require('events'); 4 | const Context = require('./context'); 5 | const utils = require('./utils'); 6 | 7 | const Project = new Class({ 8 | 9 | /** 10 | * 继承 EventEmitter 11 | **/ 12 | $extends: EventEmitter, 13 | 14 | /** 15 | * Project 的构造函数 16 | * @param {Server} server 所属 server 17 | * @param {String} name 项目名称 18 | * @param {Object} options 项目选项 19 | **/ 20 | constructor: function (server, name, options) { 21 | EventEmitter.call(this); 22 | if (!server) { 23 | throw new Error('Invalid parameters: sever'); 24 | } 25 | if (!utils.checkName(name)) { 26 | throw new Error('Invalid parameters: name'); 27 | } 28 | this.server = server; 29 | this.name = name; 30 | this.id = utils.id(this.name); 31 | this.options = options || {}; 32 | this.jobs = {}; 33 | this.allJobs = {}; 34 | }, 35 | 36 | /** 37 | * 添加或获取一个 Job 38 | * @param {String} jobName job 名称 39 | * @param {Function} runable 可以执行的函数 40 | * @param {Object} options 选项 41 | **/ 42 | job: function (jobName, runable) { 43 | if (!utils.checkName(jobName)) { 44 | throw new Error('Invalid parameter: jobName'); 45 | } 46 | if (!runable) { 47 | return this.jobs[jobName] || this.allJobs[jobName]; 48 | } 49 | var TheJob = this.defineJob(jobName, runable); 50 | this.jobs[jobName] = TheJob; 51 | return TheJob; 52 | }, 53 | 54 | /** 55 | * 解析 runable 56 | * @param {Object|Function|String} srcRunable 原始 runable 57 | **/ 58 | _parseRunable: function (srcRunable) { 59 | var self = this; 60 | //普通 runable 函数 61 | if (utils.isFunction(srcRunable)) { 62 | return { 63 | runable: srcRunable 64 | }; 65 | } 66 | //调用其它 Job 67 | if (utils.isString(srcRunable)) { 68 | return { 69 | runable: function () { 70 | return this.invoke(srcRunable, this.done.bind(this)); 71 | } 72 | }; 73 | } 74 | //包括 runable 和 register 的对象 75 | if (srcRunable.runable) { 76 | return srcRunable; 77 | } 78 | throw new Error('Invalid parameters: runable'); 79 | }, 80 | 81 | /** 82 | * 定义一个新 Job 83 | **/ 84 | defineJob: function (name, runable, parent) { 85 | if (!utils.checkName(name)) { 86 | throw new Error('Invalid parameter: name'); 87 | } 88 | if (!runable) { 89 | throw new Error('Invalid parameter: runable'); 90 | } 91 | var NewJob = new Class({ 92 | $extends: Job, 93 | name: name, 94 | isAbstract: false 95 | }); 96 | var runableObj = this._parseRunable(runable); 97 | if (!utils.isFunction(runableObj.runable)) { 98 | var ChildJob = this.defineJob(`${name}-child`, runableObj.runable, NewJob); 99 | runableObj.runable = function () { 100 | return this.invoke(ChildJob, this.done.bind(this)); 101 | }; 102 | } 103 | NewJob._runable = runableObj.runable; 104 | NewJob.prototype._runable = runableObj.runable; 105 | NewJob.prototype.Job = NewJob; 106 | NewJob.parent = parent; 107 | NewJob.__defineGetter__('name', function () { 108 | return name; 109 | }); 110 | NewJob.project = this; 111 | NewJob.server = this.server; 112 | NewJob._initSpecialDefine(); 113 | if (utils.isFunction(runableObj.register)) { 114 | runableObj.register(NewJob); 115 | } 116 | this.allJobs[name] = NewJob; 117 | return NewJob; 118 | }, 119 | 120 | /** 121 | * 隐藏的 Job 122 | **/ 123 | shadowJob: function (jobName, runable) { 124 | if (!utils.checkName(jobName)) { 125 | throw new Error('Invalid parameter: jobName'); 126 | } 127 | if (!runable) { 128 | return this.allJobs[jobName]; 129 | } 130 | return this.defineJob(jobName, runable); 131 | }, 132 | 133 | /** 134 | * 调用一个 Job 135 | * @param {String} job job 名称或类型 136 | * @param {Object} options 执行选项 137 | * @param {Function} callback 执行回完成时的回调 138 | * @param {Function} startedCallback 执行启动的回调 139 | **/ 140 | invoke: function (job, options, callback, startedCallback) { 141 | if (!callback && utils.isFunction(options)) { 142 | callback = options; 143 | options = null; 144 | } 145 | callback = callback || utils.NOOP; 146 | options = options || {}; 147 | startedCallback = startedCallback || utils.NOOP; 148 | if (!job) { 149 | callback(new Error('Invalid parameter: job')); 150 | return startedCallback(false); 151 | } 152 | var context = new Context(this, options.params); 153 | context.ready(function (err) { 154 | if (err) { 155 | callback(err); 156 | return startedCallback(false); 157 | } 158 | startedCallback(context.invoke(job, options, callback)); 159 | }); 160 | }, 161 | 162 | /** 163 | * 获取所有 job 164 | **/ 165 | getJobs: function (hasShadow) { 166 | var self = this; 167 | var jobs = hasShadow ? self.allJobs : self.jobs; 168 | return Object.keys(jobs).map(function (name) { 169 | return jobs[name]; 170 | }); 171 | }, 172 | 173 | /** 174 | * 获取 job 记录 175 | * @param {Number} limit 指定记录条数 176 | * @param {Number} skip 跳过指定的条数 177 | * @param {Function} callback 完成时的回调函数 178 | **/ 179 | getRecords: function (limit, skip, callback) { 180 | limit = limit || 100; 181 | skip = skip || 0; 182 | this.server.store.find({ 183 | projectName: this.name, 184 | refused: false, 185 | isShadow: false 186 | }, { offset: skip }, limit, ['beginTime', 'Z'], callback); 187 | }, 188 | 189 | /** 190 | * 清理记录和磁盘文件 191 | **/ 192 | clean: function (condition, callback) { 193 | condition = condition || {}; 194 | condition.projectName = this.name; 195 | return this.server.clean(condition, callback); 196 | }, 197 | 198 | /** 199 | * 调用前的钩子 200 | **/ 201 | beforeInvoke: function (jobName, hook) { 202 | return this.job(jobName).beforeRun(hook); 203 | } 204 | 205 | }); 206 | 207 | module.exports = Project; -------------------------------------------------------------------------------- /lib/series.js: -------------------------------------------------------------------------------- 1 | const async = require('async'); 2 | const Job = require('./job'); 3 | const utils = require('./utils'); 4 | const fs = require('fs'); 5 | 6 | /** 7 | * 串行步骤的任务 8 | **/ 9 | module.exports = function (array) { 10 | if (!utils.isArray(array)) { 11 | array = [].slice.call(arguments); 12 | } 13 | var NewJob, project, ChildJobs; 14 | return { 15 | register: function (_NewJob) { 16 | NewJob = _NewJob; 17 | project = _NewJob.project; 18 | var index = 0; 19 | ChildJobs = array.map(function (runable) { 20 | return NewJob.defineJob(`${NewJob.name}-${++index}`, runable); 21 | }); 22 | }, 23 | runable: function (self) { 24 | async.series(ChildJobs.map(function (ChildJob) { 25 | return function (next) { 26 | self.invoke(ChildJob, next); 27 | }; 28 | }), function (err, result) { 29 | if (self.context.hasFailed()) { 30 | err = new Error(`QueueFailed: ${err && err.message}`); 31 | } 32 | self.done(err, result); 33 | }); 34 | } 35 | } 36 | }; -------------------------------------------------------------------------------- /lib/server.js: -------------------------------------------------------------------------------- 1 | const Class = require('cify').Class; 2 | const pkg = require('../package.json'); 3 | const Project = require('./project'); 4 | const EventEmitter = require('events'); 5 | const nokit = require('nokitjs'); 6 | const path = require('path'); 7 | const utils = require('./utils'); 8 | const Store = require('./store'); 9 | const jwt = require('jwt-simple'); 10 | const async = require('async'); 11 | const mkdir = require('mkdir-p'); 12 | const rmdir = require('rmdir'); 13 | const fs = require('fs'); 14 | const os = require('os'); 15 | const exec = require('child_process').exec; 16 | 17 | const TOKEN_DEFAULT_AGE = 60 * 60 * 12; 18 | const TOKEN_MAX_AGE = 60 * 60 * 24 * 365 * 100; 19 | const DEFAULT_PROT = 9000; 20 | 21 | const Server = new Class({ 22 | 23 | /** 24 | * 继承 EventEmitter 25 | **/ 26 | $extends: EventEmitter, 27 | 28 | /** 29 | * Server 的构造函数 30 | * 可以通过 options 构造一个 CI Server 实例 31 | * @param {Object} options 'Server 选项' 32 | **/ 33 | constructor: function (options) { 34 | EventEmitter.call(this); 35 | this.options = {}; 36 | this.projects = {}; 37 | this.config(options); 38 | }, 39 | 40 | /** 41 | * 配置当前 Server 实例 42 | * @param {Object} options 'Server 选项' 43 | **/ 44 | config: function (options) { 45 | utils.copy(options || {}, this.options); 46 | if (!this.options.workspace && this.options.configFile) { 47 | this.options.workspace = path.dirname(this.options.configFile); 48 | } 49 | this.id = utils.id(this.options.name || this.options.workspace); 50 | this.paths = { 51 | data: path.normalize(`${this.options.workspace}/data/`), 52 | works: path.normalize(`${this.options.workspace}/works/`) 53 | }; 54 | this.options.port = this.options.port || DEFAULT_PROT; 55 | this.secret = this.options.secret || ''; 56 | }, 57 | 58 | /** 59 | * 添加或获取一个项目 60 | * @param {String} projectName 项目名称 61 | * @param {Object} projectOptions 项目选项 62 | **/ 63 | project: function (projectName, projectOptions) { 64 | if (!utils.checkName(projectName)) { 65 | throw new Error('Invalid parameter: projectName'); 66 | } 67 | if (!projectOptions) { 68 | return this.projects[projectName]; 69 | } 70 | this.projects[projectName] = new Project(this, projectName, projectOptions); 71 | return this.projects[projectName]; 72 | }, 73 | 74 | /** 75 | * 调用指定项目的指定 Job 76 | * @param {String} projectName 项目名称 77 | * @param {String} job job 名称或数型 78 | * @param {Object} options 执行选项 79 | * @param {Function} callback 执行回完成时的回调 80 | * @param {Function} startedCallback 执行启动的回调 81 | **/ 82 | invoke: function (projectName, job, options, callback, startedCallback) { 83 | if (!callback && utils.isFunction(options)) { 84 | callback = options; 85 | options = null; 86 | } 87 | callback = callback || utils.NOOP; 88 | options = options || {}; 89 | startedCallback = startedCallback || utils.NOOP; 90 | if (!utils.checkName(projectName) || !this.project(projectName)) { 91 | callback(new Error('Invalid parameter: projectName')); 92 | return startedCallback(false); 93 | } 94 | if (!job) { 95 | callback(new Error('Invalid parameter: job')); 96 | return startedCallback(false); 97 | } 98 | return this.project(projectName).invoke(job, options, callback, startedCallback); 99 | }, 100 | 101 | /** 102 | * 从外部调用指定项目的指定 Job,将根据 mode 调用 103 | * process: 将通过 cli 调用 104 | * docker: 将在 docker 中通过 cli 调用 105 | * @param {String} projectName 项目名称 106 | * @param {String} job job 名称或数型 107 | * @param {Object} options 执行选项 108 | * @param {Function} callback 执行回完成时的回调 109 | * @param {Function} startedCallback 执行启动的回调 110 | **/ 111 | externalInvoke: function (projectName, jobName, options, callback, startedCallback) { 112 | if (!this.options.configFile) { 113 | return this.invoke.apply(this, arguments); 114 | } 115 | switch (this.options.mode) { 116 | case 'new-process': 117 | var jsonParams = options.params ? JSON.stringify(options.params) : ''; 118 | jsonParams = jsonParams.replace(/\"/igm, '\\"'); 119 | var cmd = [ 120 | `${pkg.name} ${this.options.configFile}`, 121 | `--project ${projectName}`, 122 | `--job ${jobName}`, 123 | `--params ${jsonParams}` 124 | ].join(' '); 125 | exec(cmd, callback); 126 | return setTimeout(function () { 127 | startedCallback(true) 128 | }, 1000); 129 | case 'in-process': 130 | default: 131 | return this.invoke.apply(this, arguments); 132 | } 133 | }, 134 | 135 | /** 136 | * 启动 CI 服务 137 | **/ 138 | start: function (callback) { 139 | var self = this; 140 | if (!this.options.workspace) { 141 | throw new Error('Invalid workspace'); 142 | } 143 | callback = callback || utils.NOOP; 144 | self.store = new Store(self); 145 | self.webServer = new nokit.Server({ 146 | root: path.resolve(__dirname, '../web'), 147 | port: self.options.port 148 | }); 149 | self.webServer.ci = self; 150 | async.series([ 151 | function (done) { 152 | mkdir(self.paths.data, done) 153 | }, 154 | function (done) { 155 | mkdir(self.paths.works, done) 156 | }, 157 | function (done) { 158 | self.store.connect(done); 159 | }, 160 | function (done) { 161 | if (self.options.webServer === false) { 162 | return done(); 163 | } 164 | self.webServer.start(done); 165 | } 166 | ], function (err, info) { 167 | if (err) { 168 | console.error(err); 169 | self.emit('error', err); 170 | return callback(err); 171 | } 172 | var lastInfo = info && info[info.length - 1]; 173 | if (lastInfo && callback == utils.NOOP) { 174 | console.log(`${lastInfo} #${process.pid}`); 175 | } 176 | self.emit('ready', self); 177 | callback(err, lastInfo); 178 | }); 179 | }, 180 | 181 | /** 182 | * 停止服务 183 | **/ 184 | stop: function (callback) { 185 | return this.webServer.stop(callback); 186 | }, 187 | 188 | /** 189 | * 获取所有 project 190 | **/ 191 | getProjects: function () { 192 | var self = this; 193 | return Object.getOwnPropertyNames(self.projects).map(function (name) { 194 | return self.projects[name]; 195 | }); 196 | }, 197 | 198 | /** 199 | * 生成一个 token 200 | * @param {Object} payload 用于生成 token 的对象 201 | * @return {String} token 生成的 token 202 | **/ 203 | createToken: function (maxAge) { 204 | maxAge = maxAge || TOKEN_DEFAULT_AGE; 205 | if (!utils.isNumber(maxAge) || maxAge < 1) { 206 | maxAge = TOKEN_MAX_AGE; 207 | } 208 | var expires = Date.now() + (maxAge * 1000); 209 | return jwt.encode({ 210 | expires: expires, 211 | uuid: utils.id() 212 | }, this.secret || this.id); 213 | }, 214 | 215 | /** 216 | * 解析一个 token 217 | * @param {String} token 解析一个 token 218 | * @return {Object} 原始 payload 219 | **/ 220 | verifyToken: function (token) { 221 | if (!token) return; 222 | try { 223 | return jwt.decode(token, this.secret || this.id); 224 | } catch (err) { 225 | return; 226 | } 227 | }, 228 | 229 | /** 230 | * 生成执行上下文路径 231 | **/ 232 | getContextPaths: function (contextId) { 233 | var self = this; 234 | return { 235 | root: path.normalize([ 236 | self.paths.works, 237 | contextId 238 | ].join('/')), 239 | out: path.normalize([ 240 | self.paths.works, 241 | contextId, 242 | 'out' 243 | ].join('/')), 244 | cwd: path.normalize([ 245 | self.paths.works, 246 | contextId, 247 | 'cwd' 248 | ].join('/')), 249 | params: path.normalize([ 250 | self.paths.works, 251 | contextId, 252 | 'out', 253 | 'params.json' 254 | ].join('/')) 255 | }; 256 | }, 257 | 258 | /** 259 | * 获取 job 记录 260 | * @param {Number} limit 指定记录条数 261 | * @param {Number} skip 跳过指定的条数 262 | * @param {Function} callback 完成时的回调函数 263 | **/ 264 | getRecords: function (limit, skip, callback) { 265 | limit = limit || 100; 266 | skip = skip || 0; 267 | this.store.find({ 268 | refused: false, 269 | isShadow: false 270 | }, { offset: skip }, limit, ['beginTime', 'Z'], callback); 271 | }, 272 | 273 | /** 274 | * 清理记录和磁盘文件 275 | **/ 276 | clean: function (condition, callback) { 277 | var self = this; 278 | callback = callback || utils.NOOP; 279 | condition = condition || {}; 280 | self.store.find(condition, function (err, records) { 281 | if (err || !records || records.length < 1) { 282 | return callback(err); 283 | } 284 | var dbTasks = records.map(function (record) { 285 | return function (done) { 286 | record.remove(done); 287 | }; 288 | }); 289 | var fsTasks = records.map(function (record) { 290 | return function (done) { 291 | //有可能一个 contextId 中有多个 job 记录,所以删除 context 目录时先检查 292 | var paths = self.getContextPaths(record.contextId); 293 | fs.exists(paths.root, function (exists) { 294 | if (!exists) done(); 295 | rmdir(paths.root, done); 296 | }); 297 | }; 298 | }); 299 | var tasks = dbTasks.concat(fsTasks); 300 | async.parallel(tasks, callback); 301 | }); 302 | }, 303 | 304 | /** 305 | * 调用前的钩子 306 | **/ 307 | beforeInvoke: function (projectName, jobName, hook) { 308 | return this.project(projectName).beforeInvoke(jobName, hook); 309 | } 310 | 311 | }); 312 | 313 | module.exports = Server; -------------------------------------------------------------------------------- /lib/shell.js: -------------------------------------------------------------------------------- 1 | const shify = require('shify'); 2 | const utils = require('./utils'); 3 | 4 | /** 5 | * 执行一段 Shell 脚本 6 | * @param {String|Function} source 脚本源码 7 | * @param {Object} params 参数数据 8 | **/ 9 | module.exports = function (source, params) { 10 | return function (self) { 11 | self.params = self.params || {}; 12 | utils.copy(params, self.params); 13 | var shell = shify(source, { 14 | cwd: self.paths.cwd, 15 | env: process.env, 16 | params: self 17 | }); 18 | /** 19 | * 不能用 pipe,pipe 会自动关闭 stdout 20 | * 但是在发生异常时,还会调用 self.console.error 21 | * pipe 将会出来 Error: write after end 22 | * shell.stdout.pipe(self.stdout); 23 | **/ 24 | shell.stdout.on('data', function (data) { 25 | if (!data || data.length < 1) return; 26 | self.stdout.write(data); 27 | }); 28 | shell.on('exit', function (code) { 29 | return self.done(code > 0 ? new Error(`exit ${code}`) : null); 30 | }); 31 | }; 32 | }; -------------------------------------------------------------------------------- /lib/store.js: -------------------------------------------------------------------------------- 1 | const Class = require('cify').Class; 2 | const utils = require('./utils'); 3 | const mkdir = require('mkdir-p'); 4 | const orm = require('orm'); 5 | const exitHook = require('exit-hook2'); 6 | const fs = require('fs'); 7 | const os = require('os'); 8 | const path = require('path'); 9 | 10 | /** 11 | * 存储模块 12 | **/ 13 | const Store = new Class({ 14 | 15 | /** 16 | * Store 的构造函数 17 | * @param {server} server 实例 18 | **/ 19 | constructor: function (server) { 20 | this.server = server; 21 | this._exitPidPath = `${server.paths.data}.exit.pid` 22 | }, 23 | 24 | /** 25 | * 建立连接 26 | * @param {Function} callback 执行完成时的回调 27 | **/ 28 | connect: function (callback) { 29 | var self = this; 30 | callback = callback || utils.NOOP; 31 | var server = self.server, pkg = self.server.pkg; 32 | var connstr = `sqlite://${server.paths.data}${pkg.name}-${pkg.dbVersion}.db`; 33 | orm.connect(connstr, function (err, db) { 34 | if (err) return callback(err); 35 | self.db = db; 36 | self._defineRecord(); 37 | db.sync(function (err) { 38 | if (err) return callback(err); 39 | self._bindExitHandler(); 40 | self._handleRecordForExit(callback); 41 | }); 42 | }); 43 | }, 44 | 45 | /** 46 | * 定义 Record 47 | **/ 48 | _defineRecord: function () { 49 | var self = this; 50 | var options = { 51 | methods: { 52 | getOut: function (callback) { 53 | var record = this; 54 | var paths = self.server.getContextPaths(record.contextId); 55 | var outFile = path.normalize(`${paths.out}/${record.projectName}.${record.name}.txt`); 56 | fs.exists(outFile, function (exists) { 57 | if (!exists) { 58 | return callback(null, 'not found'); 59 | } 60 | fs.readFile(outFile, callback); 61 | }); 62 | } 63 | } 64 | }; 65 | self.Record = self.db.define('record', { 66 | _id: { type: 'text', key: true }, 67 | sn: { type: 'number' }, 68 | name: { type: 'text' }, 69 | summary: { type: 'text' }, 70 | projectName: { type: 'text' }, 71 | contextId: { type: 'text' }, 72 | beginTime: { type: 'number' }, 73 | endTime: { type: 'number' }, 74 | status: { type: 'number' }, 75 | isShadow: { type: 'boolean' }, 76 | refused: { type: 'boolean' }, 77 | processId: { type: 'number' } 78 | }, options); 79 | }, 80 | 81 | /** 82 | * 转换一个 job 为用于存储的记录 83 | * @param {Job} job job 实例 84 | **/ 85 | _convertJob: function (job) { 86 | return { 87 | _id: job.id, 88 | sn: job.sn, 89 | name: job.name, 90 | summary: job.summary || job.context.summary, 91 | projectName: job.project.name, 92 | contextId: job.context.id, 93 | beginTime: job.beginTime, 94 | endTime: job.endTime, 95 | status: job.status, 96 | isShadow: job.isShadow(), 97 | refused: job.refused, 98 | processId: process.pid 99 | }; 100 | }, 101 | 102 | /** 103 | * 更新 Job 执行记录 104 | * @param {Function} callback 执行完成时的回调 105 | **/ 106 | update: function (job, callback) { 107 | var self = this; 108 | self.Record.find({ _id: job.id }, function (err, records) { 109 | if (err) return callback(err); 110 | if (records.length < 1) return callback(); 111 | var record = records[0]; 112 | var attrs = self._convertJob(job); 113 | utils.copy(attrs, record); 114 | record.save(callback); 115 | }); 116 | }, 117 | 118 | /** 119 | * 插入 job 执行记录 120 | * @param {Job} job job 实例 121 | * @param {Function} callback 执行完成时的回调 122 | **/ 123 | insert: function (job, callback) { 124 | var self = this; 125 | callback = callback || utils.NOOP; 126 | return self.Record.find({ 127 | projectName: job.project ? job.project.name : job.projectName, 128 | name: job.name 129 | }, { offset: 0 }, 1, 130 | ['sn', 'Z'], function (err, records) { 131 | if (err) return callback(err); 132 | if (job.refused) { 133 | // refused 的 Job 的 sn 固定为 0; 134 | job.sn = 0; 135 | } else { 136 | job.sn = records && records[0] ? records[0].sn + 1 : 1; 137 | } 138 | self.Record.create(self._convertJob(job), callback); 139 | }); 140 | }, 141 | 142 | /** 143 | * 保存 Job (插入或更新) 144 | * @param {Job} job job 实例 145 | * @param {Function} callback 执行完成时的回调 146 | **/ 147 | save: function (job, callback) { 148 | var self = this; 149 | callback = callback || utils.NOOP; 150 | return self.Record.exists({ _id: job.id }, function (err, exists) { 151 | if (err) return callback(err); 152 | if (exists) { 153 | self.update(job, callback); 154 | } else { 155 | self.insert(job, callback); 156 | } 157 | }); 158 | }, 159 | 160 | /** 161 | * 查找 job 执行记录 162 | * db.find(condition).sort(sort).skip(skip).limit(limit).exec(callback); 163 | **/ 164 | find: function () { 165 | return this.Record.find.apply(this.Record, arguments); 166 | }, 167 | 168 | /** 169 | * 查找一个 job 执行记录 170 | **/ 171 | findOne: function (condition, callback) { 172 | callback = callback || utils.NOOP; 173 | return this.Record.find(condition, { offset: 0 }, 1, ['beginTime', 'Z'], 174 | function (err, records) { 175 | if (err) return callback(err); 176 | records = records || []; 177 | callback(null, records[0]); 178 | }); 179 | }, 180 | 181 | /** 182 | * 删除符合条件的记录 183 | **/ 184 | remove: function (condition, callback) { 185 | callback = callback || utils.NOOP; 186 | return this.Record.find(condition).remove(callback); 187 | }, 188 | 189 | /** 190 | * 绑定进程退出事件 191 | **/ 192 | _bindExitHandler: function () { 193 | var self = this; 194 | if (self._exitHandlerBound) return; 195 | exitHook(function () { 196 | if (!fs.existsSync(self.server.paths.data)) { 197 | return; 198 | } 199 | var pid = `${process.pid}${os.EOL}`; 200 | fs.appendFileSync(self._exitPidPath, pid); 201 | }); 202 | self._exitHandlerBound = true; 203 | }, 204 | 205 | /** 206 | * 处理退出时还在运行的任务记录 207 | **/ 208 | _handleRecordForExit: function (callback) { 209 | var self = this; 210 | callback = callback || utils.NOOP; 211 | fs.exists(self._exitPidPath, function (exists) { 212 | if (!exists) return callback(); 213 | fs.readFile(self._exitPidPath, function (err, data) { 214 | if (err || !data) return callback(err); 215 | fs.writeFile(self._exitPidPath, ''); 216 | var pids = data.toString().split(os.EOL).filter(function (pid) { 217 | return !!pid; 218 | }); 219 | if (pids.length < 1) return callback(); 220 | var status = require('./job').status; 221 | self.db.driver.execQuery( 222 | `UPDATE record SET status=? WHERE status=? AND processId IN (${pids})`, 223 | [status.FAILED, status.RUNING], 224 | callback 225 | ); 226 | }); 227 | }); 228 | } 229 | 230 | }); 231 | 232 | module.exports = Store; -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | const utils = require('ntils'); 2 | const domain = require('domain'); 3 | const md5 = require('md5'); 4 | const request = require('request'); 5 | 6 | /** 7 | * 保护执行一个函数 8 | * @param {Function} _try 执行的函数 9 | * @param {Function} _catch 错误回调函数 10 | **/ 11 | utils.try = function (_try, _catch) { 12 | if (!_try) return; 13 | if (process.env.NODE_ENV == 'development') { 14 | return _try(); 15 | } 16 | var dm = domain.create(); 17 | dm.on('error', function (err) { 18 | dm.exit(); 19 | if (_catch) _catch(err); 20 | }); 21 | dm.run(_try); 22 | }; 23 | 24 | /** 25 | * 生成一个唯一 ID 26 | * @param {String} str 计算字符串 27 | **/ 28 | utils.id = function (str) { 29 | return md5(str || utils.newGuid()).substr(5, 16); 30 | }; 31 | 32 | /** 33 | * 空函数 34 | **/ 35 | utils.NOOP = function () { }; 36 | 37 | /** 38 | * 检查名称是否合法 39 | **/ 40 | utils.checkName = function (name) { 41 | return /^[a-z0-9\_\-\$]+$/i.test(name); 42 | }; 43 | 44 | /** 45 | * 挂载 request 模块 46 | **/ 47 | utils.request = request; 48 | 49 | module.exports = utils; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cize", 3 | "version": "0.3.5", 4 | "dbVersion": "1", 5 | "description": "Cize 是一个「持续集成」工具", 6 | "main": "index.js", 7 | "bin": { 8 | "cize": "./bin/cli.js" 9 | }, 10 | "scripts": { 11 | "test": "mocha -t 5s", 12 | "cover": "istanbul cover _mocha" 13 | }, 14 | "engines": { 15 | "node": ">=4.0.0" 16 | }, 17 | "keywords": [ 18 | "ci", 19 | "continuous integration", 20 | "jenkins", 21 | "travis", 22 | "test", 23 | "build" 24 | ], 25 | "author": "Houfeng", 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/Houfeng/cize.git" 29 | }, 30 | "license": "MIT", 31 | "dependencies": { 32 | "ansi-to-html-umd": "^0.4.2", 33 | "async": "^1.5.2", 34 | "bootstrap": "^3.3.7", 35 | "chokidar": "^1.6.1", 36 | "cify": "^2.1.1", 37 | "cmdline": "^2.0.2", 38 | "console3": "^1.0.4", 39 | "cron": "^1.2.1", 40 | "exit-hook2": "^1.0.8", 41 | "font-awesome": "^4.7.0", 42 | "jquery": "^2.2.4", 43 | "js-yaml": "^3.7.0", 44 | "jwt-simple": "^0.5.1", 45 | "lodash": "^4.17.4", 46 | "md5": "^2.2.1", 47 | "mkdir-p": "0.0.7", 48 | "nokit-pjax": "^0.2.0", 49 | "nokitjs": "^1.26.3", 50 | "ntils": "^2.0.3", 51 | "orm": "^3.2.3", 52 | "request": "^2.79.0", 53 | "rmdir": "^1.2.0", 54 | "shify": "^0.1.2", 55 | "sqlite3": "^3.1.4", 56 | "stp": "0.0.2" 57 | }, 58 | "optionalDependencies": {}, 59 | "devDependencies": { 60 | "istanbul": "^0.4.4", 61 | "mocha": "^2.5.3" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /screenshot/monitor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Houfeng/cize/2a2818d80dffd11d95ccca57fcc8052881207331/screenshot/monitor.png -------------------------------------------------------------------------------- /test/by.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const rmdir = require('rmdir'); 3 | const nokit = require('nokitjs'); 4 | const execSync = require('child_process').exec; 5 | const ci = require('../'); 6 | const utils = ci.utils; 7 | 8 | nokit.Server.prototype.start = function (callback) { 9 | if (callback) callback(null, 'started'); 10 | }; 11 | 12 | describe('by', function () { 13 | 14 | var workspace = `${__dirname}/workspace`; 15 | 16 | var testProject; 17 | before(function (done) { 18 | testProject = null; 19 | testJob = null; 20 | testInstance = null; 21 | testParams = null; 22 | ci.config({ 23 | port: 8008, 24 | workspace: workspace, 25 | secret: '12345' 26 | }); 27 | testProject = ci.project('test', { 28 | git: '/test.git' 29 | }); 30 | ci.start(function (err) { 31 | if (err) throw err; 32 | done(); 33 | }); 34 | }); 35 | 36 | it('#by name', function (done) { 37 | var test2Done = false; 38 | var test1Ins = null; 39 | testProject.job('test1', function (self) { 40 | test1Ins = self; 41 | self.done(); 42 | }); 43 | testProject.job('test2', ci.by(['test1'], function (self) { 44 | test2Done = true; 45 | self.done(); 46 | })); 47 | testProject.on('job.end:test2', function (self) { 48 | assert.ok(self.target instanceof testProject.job('test1')); 49 | assert.equal(self.target, test1Ins); 50 | assert.equal(test2Done, true); 51 | done(); 52 | }); 53 | testProject.invoke('test1'); 54 | }); 55 | 56 | it('#by fullname', function (done) { 57 | var test2Done = false; 58 | testProject.job('test3', function (self) { 59 | self.done(); 60 | }); 61 | testProject.job('test4', ci.by(['test.test3'], function (self) { 62 | test2Done = true; 63 | self.done(); 64 | })); 65 | testProject.on('job.end:test4', function () { 66 | assert.equal(test2Done, true); 67 | done(); 68 | }); 69 | testProject.invoke('test3'); 70 | }); 71 | 72 | after(function (done) { 73 | setTimeout(function () { 74 | ci.clean({}, function (err) { 75 | if (err) throw err; 76 | done(); 77 | }); 78 | }, 500); 79 | }); 80 | 81 | }); -------------------------------------------------------------------------------- /test/context.test.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Houfeng/cize/2a2818d80dffd11d95ccca57fcc8052881207331/test/context.test.js -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const ci = require('../'); 3 | 4 | describe('index', function () { 5 | 6 | describe('#Error', function () { 7 | it('toString()', function () { 8 | var err = new Error('test'); 9 | err.stack = 'stack'; 10 | assert.equal(err.toString(), 'test\nstack'); 11 | }); 12 | }); 13 | 14 | describe('#global', function () { 15 | it('global', function () { 16 | assert.equal(global[ci.pkg.name], ci); 17 | }); 18 | }); 19 | 20 | }); -------------------------------------------------------------------------------- /test/job.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const rmdir = require('rmdir'); 3 | const nokit = require('nokitjs'); 4 | const execSync = require('child_process').exec; 5 | const ci = require('../'); 6 | const utils = ci.utils; 7 | 8 | nokit.Server.prototype.start = function (callback) { 9 | if (callback) callback(null, 'started'); 10 | }; 11 | 12 | describe('job', function () { 13 | 14 | var workspace = `${__dirname}/workspace`; 15 | 16 | var testProject, testJob, testInstance, testParams; 17 | before(function (done) { 18 | testProject = null; 19 | testJob = null; 20 | testInstance = null; 21 | testParams = null; 22 | ci.config({ 23 | port: 8008, 24 | workspace: workspace, 25 | secret: '12345' 26 | }); 27 | testProject = ci.project('test', { 28 | git: '/test.git' 29 | }); 30 | testProject.job('test', function (self) { 31 | testInstance = self; 32 | testParams = self.params; 33 | self.done(); 34 | }); 35 | testProject.shadowJob('shadow_test', function (self) { 36 | testInstance = self; 37 | testParams = self.params; 38 | self.done(); 39 | }); 40 | ci.start(function (err) { 41 | if (err) throw err; 42 | done(); 43 | }); 44 | }); 45 | 46 | describe('#invoke()', function () { 47 | it('invoke by name', function (done) { 48 | testParams = null; 49 | testProject.job('invoke', function (self) { 50 | self.invoke('test', self.done.bind(self)); 51 | }); 52 | testProject.invoke('invoke', { 53 | params: { name: 'test' } 54 | }, function (err) { 55 | if (err) throw err; 56 | assert.equal(testParams.name, 'test'); 57 | done(); 58 | }); 59 | }); 60 | 61 | it('invoke by fullname', function (done) { 62 | testParams = null; 63 | testProject.job('invoke', function (self) { 64 | self.invoke('test.test', self.done.bind(self)); 65 | }); 66 | testProject.invoke('invoke', { 67 | params: { name: 'test' } 68 | }, function (err) { 69 | if (err) throw err; 70 | assert.equal(testParams.name, 'test'); 71 | done(); 72 | }); 73 | }); 74 | 75 | it('invoke by Job', function (done) { 76 | testParams = null; 77 | testProject.job('invoke', function (self) { 78 | var ChildJob = self.Job.defineJob(utils.id(), function (child) { 79 | testParams = child.params; 80 | child.done(); 81 | }); 82 | self.invoke(ChildJob, self.done.bind(self)); 83 | }); 84 | testProject.invoke('invoke', { 85 | params: { name: 'test' } 86 | }, function (err) { 87 | if (err) throw err; 88 | assert.equal(testParams.name, 'test'); 89 | done(); 90 | }); 91 | }); 92 | 93 | }); 94 | 95 | 96 | describe('#beforeRun()', function () { 97 | it('sync refused', function (done) { 98 | var execed = false; 99 | testProject.job('test', function (self) { 100 | execed = true; 101 | self.done(); 102 | }).beforeRun(function (self) { 103 | return false; 104 | }); 105 | testProject.invoke('test', function (err) { 106 | assert.equal(err.message, 'Refused to run'); 107 | assert.equal(execed, false); 108 | done(); 109 | }); 110 | }); 111 | 112 | it('sync allowed', function (done) { 113 | var execed = false; 114 | testProject.job('test', function (self) { 115 | execed = true; 116 | self.done(); 117 | }).beforeRun(function (self) { 118 | return; 119 | }); 120 | testProject.invoke('test', function (err) { 121 | assert.equal(execed, true); 122 | done(); 123 | }); 124 | }); 125 | 126 | it('async refused', function (done) { 127 | var execed = false; 128 | testProject.job('test', function (self) { 129 | execed = true; 130 | self.done(); 131 | }).beforeRun(function (self, beforeRunDone) { 132 | setTimeout(function () { 133 | beforeRunDone(false); 134 | }, 10); 135 | }); 136 | testProject.invoke('test', function (err) { 137 | assert.equal(err.message, 'Refused to run'); 138 | assert.equal(execed, false); 139 | done(); 140 | }); 141 | }); 142 | 143 | it('async allowed', function (done) { 144 | var execed = false; 145 | testProject.job('test', function (self) { 146 | execed = true; 147 | self.done(); 148 | }).beforeRun(function (self, beforeRunDone) { 149 | setTimeout(function () { 150 | beforeRunDone(); 151 | }, 10); 152 | }); 153 | testProject.invoke('test', function (err) { 154 | assert.equal(execed, true); 155 | done(); 156 | }); 157 | }); 158 | 159 | }); 160 | 161 | describe('#afterRun()', function () { 162 | it('afterRun', function (done) { 163 | var execed = false; 164 | testProject.job('test', function (self) { 165 | self.done(); 166 | }).afterRun(function (self) { 167 | execed = true; 168 | }); 169 | testProject.invoke('test', function (err) { 170 | assert.equal(execed, true); 171 | done(); 172 | }); 173 | }); 174 | }); 175 | 176 | after(function (done) { 177 | setTimeout(function () { 178 | ci.clean({}, function (err) { 179 | if (err) throw err; 180 | done(); 181 | }); 182 | }, 500); 183 | }); 184 | 185 | }); -------------------------------------------------------------------------------- /test/parallel.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const rmdir = require('rmdir'); 3 | const nokit = require('nokitjs'); 4 | const execSync = require('child_process').exec; 5 | const ci = require('../'); 6 | const utils = ci.utils; 7 | 8 | nokit.Server.prototype.start = function (callback) { 9 | if (callback) callback(null, 'started'); 10 | }; 11 | 12 | describe('parallel', function () { 13 | 14 | var workspace = `${__dirname}/workspace`; 15 | 16 | var testProject; 17 | before(function (done) { 18 | testProject = null; 19 | testJob = null; 20 | testInstance = null; 21 | testParams = null; 22 | ci.config({ 23 | port: 8008, 24 | workspace: workspace, 25 | secret: '12345' 26 | }); 27 | testProject = ci.project('test', { 28 | git: '/test.git' 29 | }); 30 | ci.start(function (err) { 31 | if (err) throw err; 32 | done(); 33 | }); 34 | }); 35 | 36 | it('#parallel', function (done) { 37 | testProject.job('parallel', ci.parallel([ 38 | function (self) { 39 | setTimeout(function () { 40 | self.context.test = 1; 41 | self.done(); 42 | }, 500); 43 | }, 44 | function (self) { 45 | self.context.test = 2; 46 | self.done(); 47 | } 48 | ])); 49 | testProject.invoke('parallel', function (err, self) { 50 | if (err) throw err; 51 | assert.equal(self.context.test, 1); 52 | done(); 53 | }); 54 | }); 55 | 56 | after(function (done) { 57 | setTimeout(function () { 58 | ci.clean({}, function (err) { 59 | if (err) throw err; 60 | done(); 61 | }); 62 | }, 500); 63 | }); 64 | 65 | }); -------------------------------------------------------------------------------- /test/project.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const rmdir = require('rmdir'); 3 | const nokit = require('nokitjs'); 4 | const execSync = require('child_process').exec; 5 | const ci = require('../'); 6 | 7 | nokit.Server.prototype.start = function (callback) { 8 | if (callback) callback(null, 'started'); 9 | }; 10 | 11 | describe('project', function () { 12 | 13 | var workspace = `${__dirname}/workspace`; 14 | 15 | var testProject, testJob, testInstance, testParams; 16 | before(function (done) { 17 | testProject = null; 18 | testJob = null; 19 | testInstance = null; 20 | testParams = null; 21 | ci.config({ 22 | port: 8008, 23 | workspace: workspace, 24 | secret: '12345' 25 | }); 26 | testProject = ci.project('test', { 27 | git: '/test.git' 28 | }); 29 | testProject.job('test', function (self) { 30 | testInstance = self; 31 | testParams = self.params; 32 | self.done(); 33 | }); 34 | testProject.shadowJob('shadow_test', function (self) { 35 | testInstance = self; 36 | testParams = self.params; 37 | self.done(); 38 | }); 39 | ci.start(function (err) { 40 | if (err) throw err; 41 | done(); 42 | }); 43 | }); 44 | 45 | describe('#job()', function () { 46 | it('get job', function () { 47 | assert.equal(testProject.job('test').name, 'test'); 48 | }); 49 | it('set job', function () { 50 | var job = testProject.job('test2', function (done) { done(); }); 51 | assert.equal(job.name, 'test2'); 52 | }); 53 | }); 54 | 55 | describe('#shadowJob()', function () { 56 | it('get shadowJob', function () { 57 | assert.equal(testProject.shadowJob('shadow_test').name, 'shadow_test'); 58 | }); 59 | it('set shadowJob', function () { 60 | var job = testProject.shadowJob('shadow_test2', 'shadow_test'); 61 | assert.equal(job.name, 'shadow_test2'); 62 | }); 63 | }); 64 | 65 | describe('#getJobs()', function () { 66 | it('getJobs', function () { 67 | var jobs = testProject.getJobs(); 68 | assert.equal(jobs.length > 0, true); 69 | }); 70 | }); 71 | 72 | describe('#getRecords()', function () { 73 | it('getRecords', function (done) { 74 | testProject.clean({}, function (err) { 75 | if (err) throw err; 76 | testProject.invoke('test', { test: true }, function (err) { 77 | if (err) throw err; 78 | testProject.getRecords(100, 0, function (err, records) { 79 | if (err) throw err; 80 | assert.equal(records.length, 1); 81 | assert.equal(records[0].name, 'test'); 82 | done(); 83 | }); 84 | }); 85 | }); 86 | }); 87 | }); 88 | 89 | after(function (done) { 90 | setTimeout(function () { 91 | ci.clean({}, function (err) { 92 | if (err) throw err; 93 | done(); 94 | }); 95 | }, 500); 96 | }); 97 | 98 | }); -------------------------------------------------------------------------------- /test/series.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const rmdir = require('rmdir'); 3 | const nokit = require('nokitjs'); 4 | const execSync = require('child_process').exec; 5 | const ci = require('../'); 6 | const utils = ci.utils; 7 | 8 | nokit.Server.prototype.start = function (callback) { 9 | if (callback) callback(null, 'started'); 10 | }; 11 | 12 | describe('series', function () { 13 | 14 | var workspace = `${__dirname}/workspace`; 15 | 16 | var testProject; 17 | before(function (done) { 18 | testProject = null; 19 | testJob = null; 20 | testInstance = null; 21 | testParams = null; 22 | ci.config({ 23 | port: 8008, 24 | workspace: workspace, 25 | secret: '12345' 26 | }); 27 | testProject = ci.project('test', { 28 | git: '/test.git' 29 | }); 30 | ci.start(function (err) { 31 | if (err) throw err; 32 | done(); 33 | }); 34 | }); 35 | 36 | it('#series', function (done) { 37 | testProject.job('series', ci.series([ 38 | function (self) { 39 | setTimeout(function () { 40 | self.context.test = 1; 41 | self.done(); 42 | }, 500); 43 | }, 44 | function (self) { 45 | self.context.test = 2; 46 | self.done(); 47 | } 48 | ])); 49 | testProject.invoke('series', function (err, self) { 50 | if (err) throw err; 51 | assert.equal(self.context.test, 2); 52 | done(); 53 | }); 54 | }); 55 | 56 | after(function (done) { 57 | setTimeout(function () { 58 | ci.clean({}, function (err) { 59 | if (err) throw err; 60 | done(); 61 | }); 62 | }, 500); 63 | }); 64 | 65 | }); -------------------------------------------------------------------------------- /test/server.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const rmdir = require('rmdir'); 3 | const nokit = require('nokitjs'); 4 | const execSync = require('child_process').exec; 5 | const ci = require('../'); 6 | 7 | nokit.Server.prototype.start = function (callback) { 8 | if (callback) callback(null, 'started'); 9 | }; 10 | 11 | describe('server', function () { 12 | 13 | var workspace = `${__dirname}/workspace`; 14 | 15 | var testProject, testJob, testInstance, testParams; 16 | before(function (done) { 17 | testProject = null; 18 | testJob = null; 19 | testInstance = null; 20 | testParams = null; 21 | ci.config({ 22 | port: 8008, 23 | workspace: workspace, 24 | secret: '12345' 25 | }); 26 | testProject = ci.project('test', { 27 | git: '/test.git' 28 | }); 29 | testProject.job('test', function (self) { 30 | testInstance = self; 31 | testParams = self.params; 32 | self.done(); 33 | }); 34 | ci.start(function (err) { 35 | if (err) throw err; 36 | done(); 37 | }); 38 | }); 39 | 40 | describe('#config()', function () { 41 | it('config', function () { 42 | assert.equal(ci.paths.data, `${workspace}/data/`); 43 | assert.equal(ci.paths.works, `${workspace}/works/`); 44 | assert.equal(ci.options.port, 8008); 45 | }); 46 | }); 47 | 48 | describe('#project()', function () { 49 | it('define project', function () { 50 | assert.equal(testProject.name, 'test'); 51 | assert.equal(testProject.options.git, '/test.git'); 52 | }); 53 | }); 54 | 55 | describe('#invoke()', function () { 56 | it('define project', function (done) { 57 | ci.invoke('test', 'test', { 58 | params: { test: true } 59 | }, function (err) { 60 | if (err) throw err; 61 | assert.equal(testInstance.name, 'test'); 62 | assert.equal(testParams.test, true); 63 | done(); 64 | }); 65 | }); 66 | }); 67 | 68 | describe('#getProjects()', function () { 69 | it('getProjects', function () { 70 | assert.equal(ci.getProjects().length > 0, true); 71 | }); 72 | }); 73 | 74 | describe('#getRecords()', function () { 75 | it('getRecords', function (done) { 76 | ci.invoke('test', 'test', { test: true }, function (err) { 77 | if (err) throw err; 78 | ci.getRecords(1, 0, function (err, records) { 79 | if (err) throw err; 80 | assert.equal(records[0].name, 'test'); 81 | done(); 82 | }); 83 | }); 84 | }); 85 | }); 86 | 87 | describe('#clean()', function () { 88 | it('clean', function (done) { 89 | ci.clean({}, function (err) { 90 | if (err) throw err; 91 | ci.getRecords(1, 0, function (err, records) { 92 | if (err) throw err; 93 | assert.equal(records.length, 0); 94 | done(); 95 | }); 96 | }); 97 | }); 98 | }); 99 | 100 | describe('#token', function () { 101 | it('createToken & verifyToken', function () { 102 | var token = ci.createToken(1); 103 | assert.equal(token != null, true); 104 | var payload = ci.verifyToken(token); 105 | assert.equal(payload != null, true); 106 | }); 107 | }); 108 | 109 | describe('#beforeInvoke()', function () { 110 | it('beforeInvoke()', function (done) { 111 | testParams = null; 112 | ci.beforeInvoke('test', 'test', function () { 113 | return false; 114 | }); 115 | ci.invoke('test', 'test', { test: true }, function (err) { 116 | assert.equal(testParams, null); 117 | done(); 118 | }); 119 | }); 120 | }); 121 | 122 | after(function (done) { 123 | setTimeout(function () { 124 | ci.clean({}, function (err) { 125 | if (err) throw err; 126 | done(); 127 | }); 128 | }, 500); 129 | }); 130 | 131 | }); -------------------------------------------------------------------------------- /test/shell.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const rmdir = require('rmdir'); 3 | const nokit = require('nokitjs'); 4 | const execSync = require('child_process').exec; 5 | const ci = require('../'); 6 | const utils = ci.utils; 7 | 8 | nokit.Server.prototype.start = function (callback) { 9 | if (callback) callback(null, 'started'); 10 | }; 11 | 12 | describe('shell', function () { 13 | 14 | var workspace = `${__dirname}/workspace`; 15 | 16 | var testProject; 17 | before(function (done) { 18 | testProject = null; 19 | testJob = null; 20 | testInstance = null; 21 | testParams = null; 22 | ci.config({ 23 | port: 8008, 24 | workspace: workspace, 25 | secret: '12345' 26 | }); 27 | testProject = ci.project('test', { 28 | git: '/test.git' 29 | }); 30 | ci.start(function (err) { 31 | if (err) throw err; 32 | done(); 33 | }); 34 | }); 35 | 36 | it('#shell', function (done) { 37 | testProject.job('shell', ci.shell(function () { 38 | /* 39 | exit 0 40 | */ 41 | })); 42 | testProject.invoke('shell', function (err) { 43 | if (err) throw err; 44 | done(); 45 | }); 46 | }); 47 | 48 | after(function (done) { 49 | setTimeout(function () { 50 | ci.clean({}, function (err) { 51 | if (err) throw err; 52 | done(); 53 | }); 54 | }, 500); 55 | }); 56 | 57 | }); -------------------------------------------------------------------------------- /web/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 自动生成的应用入口程序 3 | * 4 | * 使用 nokit start 命令启动时,会忽略此入口程序 5 | * 通常以下情况使用此入口程序: 6 | * 1) 在使用进程管理工具(pm2等)时 7 | * 2) 目标 "环境" 无法使用 nokit start 命令时 8 | * 9 | * 确保添加了对 nokit 的依赖,或全局安装了 nokit 并设置了 NODE_PATH 环境变量 10 | * 11 | * 安装命令: 12 | * npm install nokitjs [-g] 13 | **/ 14 | 15 | /* global __dirname */ 16 | 17 | const nokit = require("nokitjs"); 18 | 19 | /** 20 | * 创建 server 实例 21 | **/ 22 | const server = new nokit.Server({ 23 | "root": __dirname 24 | }); 25 | 26 | /** 27 | * 启动 server 28 | **/ 29 | server.start(function (err, msg) { 30 | if (err) { 31 | console.error(err); 32 | } else { 33 | console.log(msg); 34 | } 35 | }); -------------------------------------------------------------------------------- /web/common/utils.js: -------------------------------------------------------------------------------- 1 | const utils = nokit.utils; 2 | utils.pkg = require('../../package.json'); 3 | const Convert = require('ansi-to-html-umd'); 4 | 5 | const convert = new Convert({ 6 | fg: '#333', 7 | bg: '#fff' 8 | }); 9 | 10 | utils.statusIcons = { 11 | "300": "fa-exclamation-circle", 12 | "200": "fa-check-circle", 13 | "100": "fa-spinner fa-spin fa-fw" 14 | }; 15 | 16 | utils.ansiToHtml = function (data) { 17 | if (!data) return data; 18 | data = data.toString(); 19 | return convert.toHtml(data.replace(/\[2K\[0G/igm, '')); 20 | } 21 | 22 | module.exports = utils; -------------------------------------------------------------------------------- /web/config.development.yaml: -------------------------------------------------------------------------------- 1 | showErrorDetail: true -------------------------------------------------------------------------------- /web/config.production.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Houfeng/cize/2a2818d80dffd11d95ccca57fcc8052881207331/web/config.production.yaml -------------------------------------------------------------------------------- /web/config.yaml: -------------------------------------------------------------------------------- 1 | handlers: 2 | ^/: $./handlers/mvc 3 | 4 | filters: 5 | ^/@pjax: ../node_modules/nokit-pjax 6 | ^/@auth: ./filters/auth 7 | 8 | mvc: 9 | routes: 10 | /auth: 11 | target: ./auth 12 | ignoreAuth: true 13 | /: ./main 14 | /projects/{project}/jobs/{job}/records/{sn:^[0-9]+$}: ./main 15 | /projects/{project}/jobs/{job}: ./main 16 | /projects/{project}: ./main 17 | get /api: ./api 18 | post /api/token: ./api token 19 | post /api/projects/{project}/jobs/{job}/trigger: ./api trigger 20 | get /api/projects: ./api projects 21 | get /api/records: ./api serverRecords 22 | get /api/projects/{project}: ./api project 23 | get /api/projects/{project}/jobs: ./api jobs 24 | get /api/projects/{project}/records: ./api projectRecords 25 | get /api/projects/{project}/jobs/{job}: ./api job 26 | get /api/projects/{project}/jobs/{job}/records: ./api records 27 | get /api/projects/{project}/jobs/{job}/records/{sn:^[0-9]+$}: ./api record 28 | get /api/projects/{project}/jobs/{job}/records/{sn:^[0-9]+$}/console: ./api console 29 | post /api/projects/{project}/jobs/{job}/records/{sn:^[0-9]+$}/rerun: ./api rerun 30 | 31 | public: 32 | ^/bootstrap(.*): ../node_modules/bootstrap/dist 33 | ^/jquery(.*): ../node_modules/jquery/dist 34 | ^/font-awesome(.*): ../node_modules/font-awesome 35 | ^/pjax/(.*)$: ../node_modules/nokit-pjax/client 36 | ^/js-yaml/(.*)$: ../node_modules/js-yaml/dist 37 | 38 | session: 39 | timeout: 7200 -------------------------------------------------------------------------------- /web/controllers/api.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | const utils = require('../common/utils'); 3 | 4 | /** 5 | * ApiController 6 | **/ 7 | const ApiController = nokit.define({ 8 | 9 | /** 10 | * 初始化方法,每次请求都会先执行 init 方法 11 | **/ 12 | init: function () { 13 | this.ci = this.server.ci; 14 | this.projectName = this.context.params.project; 15 | this.jobName = this.context.params.job; 16 | this.recordSn = this.context.params.sn; 17 | this.params = this.context.request.body || this.context.request.query; 18 | this.ready(); 19 | }, 20 | 21 | /** 22 | * 响应数据 23 | **/ 24 | send: function (status, data) { 25 | this.context.status(status); 26 | this.context.json(data); 27 | }, 28 | 29 | /** 30 | * 默认 action 31 | **/ 32 | index: function () { 33 | this.send(200, { 34 | name: this.ci.pkg.name, 35 | version: this.ci.pkg.version, 36 | links: [] 37 | }); 38 | }, 39 | 40 | /** 41 | * 生成 token 42 | **/ 43 | token: function () { 44 | var token = this.ci.createToken(this.context.param('maxAge')); 45 | this.send(200, { 46 | token: token 47 | }); 48 | }, 49 | 50 | /** 51 | * 默认 action 52 | **/ 53 | trigger: function (context, params) { 54 | var self = this; 55 | self.server.ci.externalInvoke( 56 | self.projectName, 57 | self.jobName, 58 | { 59 | params: params || self.params 60 | }, 61 | null, 62 | function (started) { 63 | self.send(started ? 202 : 400, { 64 | message: started ? 'Job is triggered' : 'Trigger failed' 65 | }); 66 | } 67 | ); 68 | }, 69 | 70 | /** 71 | * 查询所有项目 72 | **/ 73 | projects: function () { 74 | this.send(200, this.ci.getProjects().map(function (item) { 75 | return { 76 | id: item.id, 77 | name: item.name 78 | }; 79 | })); 80 | }, 81 | 82 | /** 83 | * 查询一个项目 84 | **/ 85 | project: function () { 86 | var project = this.ci.project(this.projectName); 87 | if (!project) { 88 | return this.send(404, { 89 | message: 'Project not found' 90 | }); 91 | } 92 | this.send(200, { 93 | id: project.id, 94 | name: project.name 95 | }); 96 | }, 97 | 98 | /** 99 | * 查询所有 jobs 100 | **/ 101 | jobs: function () { 102 | var project = this.ci.project(this.projectName); 103 | if (!project) { 104 | return this.send(404, { 105 | message: 'Project not found' 106 | }); 107 | } 108 | var jobs = project.getJobs(!!this.context.query.shadow); 109 | this.send(200, jobs.map(function (item) { 110 | return { 111 | id: item.id, 112 | name: item.name, 113 | isShadow: item.isShadow() 114 | }; 115 | })); 116 | }, 117 | 118 | /** 119 | * 查询一个 job 120 | **/ 121 | job: function () { 122 | var project = this.ci.project(this.projectName); 123 | if (!project) { 124 | return this.send(404, { 125 | message: 'Project not found' 126 | }); 127 | } 128 | var job = project.job(this.jobName); 129 | if (!job) { 130 | return this.send(404, { 131 | message: 'Job not found' 132 | }); 133 | } 134 | this.send(200, { 135 | id: job.id, 136 | name: job.name, 137 | isShadow: job.isShadow() 138 | }); 139 | }, 140 | 141 | /** 142 | * 查询所有记录 143 | **/ 144 | records: function () { 145 | var project = this.ci.project(this.projectName); 146 | if (!project) { 147 | return this.send(404, { 148 | message: 'Project not found' 149 | }); 150 | } 151 | var job = project.job(this.jobName); 152 | if (!job) { 153 | return this.send(404, { 154 | message: 'Job not found' 155 | }); 156 | } 157 | var limit = Number(this.context.query.limit || 100); 158 | var skip = Number(this.context.query.skip || 0); 159 | job.getRecords(limit, skip, function (err, records) { 160 | if (err) { 161 | return this.send(500, { 162 | message: err.message 163 | }); 164 | } 165 | this.send(200, records); 166 | }.bind(this)); 167 | }, 168 | 169 | /** 170 | * 查询整个服务所有记录 171 | **/ 172 | serverRecords: function () { 173 | var limit = Number(this.context.query.limit || 100); 174 | var skip = Number(this.context.query.skip || 0); 175 | this.ci.getRecords(limit, skip, function (err, records) { 176 | if (err) { 177 | return this.send(500, { 178 | message: err.message 179 | }); 180 | } 181 | this.send(200, records); 182 | }.bind(this)); 183 | }, 184 | 185 | /** 186 | * 查询指定项目所有记录 187 | **/ 188 | projectRecords: function () { 189 | var project = this.ci.project(this.projectName); 190 | if (!project) { 191 | return this.send(404, { 192 | message: 'Project not found' 193 | }); 194 | } 195 | var limit = Number(this.context.query.limit || 100); 196 | var skip = Number(this.context.query.skip || 0); 197 | project.getRecords(limit, skip, function (err, records) { 198 | if (err) { 199 | return this.send(500, { 200 | message: err.message 201 | }); 202 | } 203 | this.send(200, records); 204 | }.bind(this)); 205 | }, 206 | 207 | /** 208 | * 查询一条记录 209 | **/ 210 | record: function (context, callback) { 211 | var project = this.ci.project(this.projectName); 212 | if (!project) { 213 | return this.send(404, { 214 | message: 'Project not found' 215 | }); 216 | } 217 | var job = project.job(this.jobName); 218 | if (!job) { 219 | return this.send(404, { 220 | message: 'Job not found' 221 | }); 222 | } 223 | job.getRecordBySn(this.recordSn, function (err, record) { 224 | if (err) { 225 | return this.send(500, { 226 | message: err.message 227 | }); 228 | } 229 | if (!record) { 230 | return this.send(404, { 231 | message: 'Record not found' 232 | }); 233 | } 234 | if (callback) { 235 | return callback(err, record); 236 | } 237 | this.send(200, record); 238 | }.bind(this)); 239 | }, 240 | 241 | /** 242 | * 显示控制台输出 243 | **/ 244 | console: function (context) { 245 | var self = this; 246 | self.record(context, function (err, record) { 247 | record.getOut(function (err, data) { 248 | if (err) { 249 | return this.send(500, { 250 | message: err.message 251 | }); 252 | } 253 | self.send(200, { 254 | status: record.status, 255 | out: utils.ansiToHtml(data) 256 | }); 257 | }); 258 | }); 259 | }, 260 | 261 | /** 262 | * 重新运行一个 job 263 | **/ 264 | rerun: function (context) { 265 | var self = this; 266 | self.record(context, function (err, record) { 267 | var paths = self.ci.getContextPaths(record.contextId); 268 | fs.readFile(paths.params, function (err, data) { 269 | if (err) { 270 | return self.send(400, { 271 | message: 'Trigger failed' 272 | }); 273 | } 274 | self.trigger(context, JSON.parse((data || '{}').toString())); 275 | }); 276 | }); 277 | } 278 | 279 | }); 280 | 281 | module.exports = ApiController; -------------------------------------------------------------------------------- /web/controllers/auth.js: -------------------------------------------------------------------------------- 1 | const TOKEN_MAX_AGE = 1000 * 60 * 60 * 12; 2 | 3 | /** 4 | * AuthController 5 | **/ 6 | const AuthController = nokit.define({ 7 | 8 | /** 9 | * 初始化方法,每次请求都会先执行 init 方法 10 | **/ 11 | init: function () { 12 | var self = this; 13 | self.ready(); 14 | }, 15 | 16 | /** 17 | * 默认 action 18 | **/ 19 | index: function () { 20 | var self = this; 21 | self.render('auth', self); 22 | }, 23 | 24 | /** 25 | * 登录 26 | **/ 27 | login: function () { 28 | var self = this; 29 | if (self.context.form.secret == self.server.ci.secret) { 30 | var token = self.server.ci.createToken(TOKEN_MAX_AGE); 31 | self.context.cookie.set(self.context.tokenKey, token, { 32 | "Max-Age": TOKEN_MAX_AGE 33 | }); 34 | self.context.redirect('/'); 35 | } else { 36 | //self.message = 'xxx'; 37 | self.render('auth', self); 38 | } 39 | }, 40 | 41 | /** 42 | * 登出 43 | **/ 44 | logout: function () { 45 | var self = this; 46 | self.context.cookie.remove(self.context.tokenKey); 47 | self.context.redirect('/auth'); 48 | } 49 | 50 | }); 51 | 52 | module.exports = AuthController; -------------------------------------------------------------------------------- /web/controllers/main.js: -------------------------------------------------------------------------------- 1 | const async = require('async'); 2 | const utils = require('../common/utils'); 3 | 4 | /** 5 | * 定义 MainController 6 | **/ 7 | const MainController = nokit.define({ 8 | 9 | /** 10 | * 初始化方法,每次请求都会先执行 init 方法 11 | **/ 12 | init: function* () { 13 | var self = this; 14 | self.ci = self.server.ci; 15 | async.series([ 16 | self._loadProjects.bind(self), 17 | self._loadJobs.bind(self), 18 | self._loadLastRecordOfProjectsAndJobs.bind(self), 19 | function (done) { 20 | self.sn = self.context.params.sn; 21 | if (self.sn) { 22 | self._loadOneRecord(self.sn, done); 23 | } else { 24 | self._loadRecords(done); 25 | } 26 | } 27 | ], self.ready.bind(self)); 28 | }, 29 | 30 | /** 31 | * 加载项目 32 | **/ 33 | _loadProjects: function (callback) { 34 | var self = this; 35 | //项目 36 | self.projects = self.ci.projects; 37 | self.projectName = self.context.params.project || Object.getOwnPropertyNames(self.projects)[0]; 38 | if (!self.projectName) { 39 | return self.render('alert'); 40 | } 41 | self.project = self.ci.project(self.projectName); 42 | if (!self.project) { 43 | return self.context.notFound(); 44 | } 45 | if (callback) callback(); 46 | }, 47 | 48 | /** 49 | * 加载 Job 50 | **/ 51 | _loadJobs: function (callback) { 52 | var self = this; 53 | //job 54 | self.jobs = self.project.jobs; 55 | self.jobName = self.context.params.job || Object.getOwnPropertyNames(self.jobs)[0]; 56 | if (!self.jobName) { 57 | return self.render('alert'); 58 | } 59 | self.job = self.project.job(self.jobName); 60 | if (!self.job) { 61 | return self.context.notFound(); 62 | } 63 | if (callback) callback(); 64 | }, 65 | 66 | /** 67 | * 获取所有项目和当前项目下每个 job 最后一次执行记录 68 | **/ 69 | _loadLastRecordOfProjectsAndJobs: function (callback) { 70 | var self = this; 71 | var items = self.ci.getProjects().concat(self.project.getJobs()); 72 | async.parallel(items.map(function (item) { 73 | return function (done) { 74 | item.getRecords(1, 0, function (err, records) { 75 | if (err) return done(err); 76 | item.lastRecord = records[0]; 77 | done(); 78 | }); 79 | }; 80 | }), callback); 81 | }, 82 | 83 | /** 84 | * 加载记录 85 | **/ 86 | _loadRecords: function (callback) { 87 | var self = this; 88 | self.job.getRecords(100, 0, function (err, records) { 89 | if (err) return self.context.error(err); 90 | self.records = records; 91 | if (callback) callback(); 92 | }); 93 | }, 94 | 95 | /** 96 | * 加载一条记录 97 | **/ 98 | _loadOneRecord: function (sn, callback) { 99 | var self = this; 100 | self.job.getRecordBySn(sn, function (err, record) { 101 | if (err) return callback(err); 102 | if (!record) return self.context.notFound(); 103 | self.record = record; 104 | self.record.getOut(function (err, data) { 105 | if (err) return callback(err); 106 | self.record.out = utils.ansiToHtml(data); 107 | callback(); 108 | }); 109 | }); 110 | }, 111 | 112 | /** 113 | * 默认 action 114 | **/ 115 | index: function () { 116 | this.render("main", this); 117 | } 118 | 119 | }); 120 | 121 | module.exports = MainController; -------------------------------------------------------------------------------- /web/filters/auth.js: -------------------------------------------------------------------------------- 1 | const GENERAL_TOKEN_KEY = 'token'; 2 | 3 | /** 4 | * AuthFilter 5 | **/ 6 | const AuthFilter = nokit.define({ 7 | 8 | /** 9 | * 在处理 MVC 请求时 10 | **/ 11 | onMvcHandle: function (context, next) { 12 | var ci = context.server.ci; 13 | //-- 14 | context.tokenKey = `${ci.pkg.name}-token`; 15 | context.getToken = function (tokenKey) { 16 | tokenKey = tokenKey || this.tokenKey; 17 | return this.request.headers[tokenKey] || 18 | this.param(tokenKey) || 19 | this.cookie.get(tokenKey); 20 | }; 21 | //-- 22 | if (context.route.ignoreAuth || !ci.secret) { 23 | return next(); 24 | } 25 | var token = context.getToken() || context.getToken(GENERAL_TOKEN_KEY); 26 | var payload = ci.verifyToken(token); 27 | if (payload && payload.expires > Date.now()) { 28 | context.user = payload; 29 | next(); 30 | } else if (/^\/api/igm.test(context.request.url)) { 31 | context.status(401); 32 | return context.json({ 33 | message: 'Authentication failed' 34 | }); 35 | } else { 36 | return context.redirect('/auth'); 37 | } 38 | } 39 | 40 | }); 41 | 42 | //exports 43 | module.exports = AuthFilter; -------------------------------------------------------------------------------- /web/global.js: -------------------------------------------------------------------------------- 1 | var utils = require('./common/utils'); 2 | 3 | /** 4 | * 全局应用程序类 5 | **/ 6 | const Global = nokit.define({}); 7 | 8 | /** 9 | * export 10 | **/ 11 | module.exports = Global; -------------------------------------------------------------------------------- /web/locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "projects": "Projects", 3 | "jobs": "Jobs", 4 | "records": "Records", 5 | "monitor": "Monitor", 6 | "auth": "Authentication", 7 | "exit": "Exit", 8 | "secret": "Secret", 9 | "home": "Home", 10 | "document": "Docs", 11 | "confirm": "Confirm", 12 | "console": "Console", 13 | "back": "Back", 14 | "trigger": "Trigger", 15 | "cancel": "Cancel", 16 | "close": "Close", 17 | "params_format": "YAML/JSON ...", 18 | "token": "Token", 19 | "alert": "Alert", 20 | "noProjectsOrJobs": "No projects or jobs", 21 | "rerun": "Rerun", 22 | "setting": "Setting", 23 | "generate_token": "Generate token", 24 | "token_max_age": "Token max age (day)", 25 | "no_record": "No record" 26 | } -------------------------------------------------------------------------------- /web/locales/zh-hk.json: -------------------------------------------------------------------------------- 1 | { 2 | "projects": "項目", 3 | "jobs": "任務", 4 | "records": "記錄", 5 | "monitor": "監視器", 6 | "auth": "認證", 7 | "exit": "退出", 8 | "secret": "金鑰", 9 | "home": "主頁", 10 | "document": "檔案", 11 | "confirm": "確認", 12 | "console": "控制台", 13 | "back": "返回", 14 | "trigger": "觸發", 15 | "cancel": "取消", 16 | "close": "關閉", 17 | "params_format": "YAML/JSON ...", 18 | "token": "金鑰", 19 | "alert": "提示", 20 | "noProjectsOrJobs": "沒有項目或任務", 21 | "rerun": "重新運行", 22 | "setting": "設定", 23 | "generate_token": "生成金鑰", 24 | "token_max_age": "金鑰有效期(天)", 25 | "no_record": "沒有記錄" 26 | } -------------------------------------------------------------------------------- /web/locales/zh-sg.json: -------------------------------------------------------------------------------- 1 | { 2 | "projects": "项目", 3 | "jobs": "任务", 4 | "records": "记录", 5 | "monitor": "监视器", 6 | "auth": "认证", 7 | "exit": "退出", 8 | "secret": "密钥", 9 | "home": "主页", 10 | "document": "文档", 11 | "confirm": "确认", 12 | "console": "控制台", 13 | "back": "返回", 14 | "trigger": "触发", 15 | "cancel": "取消", 16 | "close": "关闭", 17 | "params_format": "YAML/JSON ...", 18 | "token": "密钥", 19 | "alert": "提示", 20 | "noProjectsOrJobs": "没有项目或任务", 21 | "rerun": "重新运行", 22 | "setting": "设置", 23 | "generate_token": "生成密钥", 24 | "token_max_age": "密钥有效期 (天)", 25 | "no_record": "没有记录" 26 | } -------------------------------------------------------------------------------- /web/locales/zh-tw.json: -------------------------------------------------------------------------------- 1 | { 2 | "projects": "項目", 3 | "jobs": "任務", 4 | "records": "記錄", 5 | "monitor": "監視器", 6 | "auth": "認證", 7 | "exit": "退出", 8 | "secret": "金鑰", 9 | "home": "主頁", 10 | "document": "檔案", 11 | "confirm": "確認", 12 | "console": "控制台", 13 | "back": "返回", 14 | "trigger": "觸發", 15 | "cancel": "取消", 16 | "close": "關閉", 17 | "params_format": "YAML/JSON ...", 18 | "token": "金鑰", 19 | "alert": "提示", 20 | "noProjectsOrJobs": "沒有項目或任務", 21 | "rerun": "重新運行", 22 | "setting": "設定", 23 | "generate_token": "生成金鑰", 24 | "token_max_age": "金鑰有效期(天)", 25 | "no_record": "沒有記錄" 26 | } -------------------------------------------------------------------------------- /web/locales/zh.json: -------------------------------------------------------------------------------- 1 | { 2 | "projects": "项目", 3 | "jobs": "任务", 4 | "records": "记录", 5 | "monitor": "监视器", 6 | "auth": "认证", 7 | "exit": "退出", 8 | "secret": "密钥", 9 | "home": "主页", 10 | "document": "文档", 11 | "confirm": "确认", 12 | "console": "控制台", 13 | "back": "返回", 14 | "trigger": "触发", 15 | "cancel": "取消", 16 | "close": "关闭", 17 | "params_format": "YAML/JSON ...", 18 | "token": "密钥", 19 | "alert": "提示", 20 | "noProjectsOrJobs": "没有项目或任务", 21 | "rerun": "重新运行", 22 | "setting": "设置", 23 | "generate_token": "生成密钥", 24 | "token_max_age": "密钥有效期 (天)", 25 | "no_record": "没有记录" 26 | } -------------------------------------------------------------------------------- /web/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Houfeng/cize/2a2818d80dffd11d95ccca57fcc8052881207331/web/models/.gitkeep -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cize-web", 3 | "version": "1.0.0", 4 | "description": "cize-web", 5 | "main": "./app.js", 6 | "keywords": [ 7 | "nokitjs", 8 | "nokit", 9 | "mvc" 10 | ], 11 | "author": { 12 | "name": "Houfeng", 13 | "email": "admin@xhou.net" 14 | }, 15 | "homepage": "http://nokit.org", 16 | "license": "MIT", 17 | "dependencies": { 18 | "nokitjs": "^1.24.0" 19 | } 20 | } -------------------------------------------------------------------------------- /web/public/css/common.css: -------------------------------------------------------------------------------- 1 | html, body, .container-main { 2 | height: 100%; 3 | padding: 0; 4 | margin: 0; 5 | } 6 | 7 | * { 8 | font-family: Arial, "Helvetica Neue", Helvetica, sans-serif; 9 | -webkit-font-smoothing: antialiased; 10 | } 11 | 12 | body { 13 | padding-top: 50px; 14 | background-color: #fefefe; 15 | } 16 | 17 | .panel-default>.panel-heading a { 18 | color: #333; 19 | } 20 | 21 | .dashboard, .dashboard-col { 22 | height: 100%; 23 | padding: 0; 24 | margin: 0; 25 | border-radius: 0; 26 | } 27 | 28 | .dashboard-col { 29 | border-right: 0; 30 | border-bottom: 0; 31 | padding-top: 40px; 32 | } 33 | 34 | .dashboard-col .panel-heading { 35 | position: absolute; 36 | width: 100%; 37 | left: 0; 38 | top: 0; 39 | z-index: 1; 40 | } 41 | 42 | .dashboard-col:first-child { 43 | border-left: 0; 44 | } 45 | 46 | .dashboard .list-group, .dashboard .console-wraper { 47 | border-radius: 0; 48 | height: 100%; 49 | overflow: auto; 50 | } 51 | 52 | .dashboard .list-group-item { 53 | border-radius: 0 !important; 54 | cursor: pointer; 55 | border: 1px solid #e9e9e9; 56 | } 57 | 58 | .navbar-brand { 59 | padding: 8px; 60 | } 61 | 62 | .list-group-item-heading { 63 | font-size: 16px; 64 | } 65 | 66 | .list-group-item-heading .fa { 67 | margin: 0 0 0 12px; 68 | color: #888; 69 | } 70 | 71 | .list-group-item-text { 72 | font-size: 12px; 73 | color: #888; 74 | } 75 | 76 | .console-wraper { 77 | position: relative; 78 | padding: 83px 0 0 0; 79 | margin: 0; 80 | } 81 | 82 | .console-wraper .console { 83 | margin: 0; 84 | background: #fff; 85 | height: 100%; 86 | overflow: auto; 87 | padding: 8px; 88 | color: #333; 89 | border-radius: 0; 90 | border: none; 91 | } 92 | 93 | .console-wraper .current-info { 94 | position: absolute; 95 | left: 0; 96 | top: 0; 97 | width: 100%; 98 | height: auto; 99 | } 100 | 101 | .console-wraper .current-info, .console-wraper .current-info .list-group-item { 102 | border: none; 103 | cursor: default; 104 | background: #f9f9f9; 105 | } 106 | 107 | .console-wraper .current-info .list-group-item { 108 | border-bottom: solid 1px #e9e9e9; 109 | } 110 | 111 | .console-wraper .console, .console-wraper .console * { 112 | font-family: "Courier New", Courier, "Lucida Sans Typewriter", "Lucida Typewriter", monospace; 113 | } 114 | 115 | .panel-auth, .panel-alert { 116 | max-width: 500px; 117 | margin: 100px auto; 118 | } 119 | 120 | .panel-alert .panel-body { 121 | text-align: center; 122 | } 123 | 124 | .navbar-header { 125 | position: relative; 126 | } 127 | 128 | .version { 129 | position: absolute; 130 | bottom: 16px; 131 | left: 82px; 132 | color: #aaa; 133 | } 134 | 135 | .clickable { 136 | cursor: pointer; 137 | } 138 | 139 | .clickable:hover { 140 | text-decoration: underline; 141 | } 142 | 143 | .navbar-toggle { 144 | padding: 5px 10px; 145 | } 146 | 147 | @media (max-width: 990px) { 148 | .dashboard-col { 149 | height: auto; 150 | } 151 | .panel-auth { 152 | margin: 100px 15px; 153 | } 154 | } 155 | 156 | @media (max-width: 768px) { 157 | .version { 158 | left: 100px; 159 | } 160 | } 161 | 162 | input.danger, textarea.danger { 163 | border-color: #a94442 !important; 164 | } 165 | 166 | .form-row { 167 | margin-bottom: 10px; 168 | } 169 | 170 | .form-row:last-child { 171 | margin-bottom: 0; 172 | } 173 | 174 | .well { 175 | word-break: break-all; 176 | } -------------------------------------------------------------------------------- /web/public/favicons/android-chrome-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Houfeng/cize/2a2818d80dffd11d95ccca57fcc8052881207331/web/public/favicons/android-chrome-144x144.png -------------------------------------------------------------------------------- /web/public/favicons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Houfeng/cize/2a2818d80dffd11d95ccca57fcc8052881207331/web/public/favicons/android-chrome-192x192.png -------------------------------------------------------------------------------- /web/public/favicons/android-chrome-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Houfeng/cize/2a2818d80dffd11d95ccca57fcc8052881207331/web/public/favicons/android-chrome-36x36.png -------------------------------------------------------------------------------- /web/public/favicons/android-chrome-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Houfeng/cize/2a2818d80dffd11d95ccca57fcc8052881207331/web/public/favicons/android-chrome-48x48.png -------------------------------------------------------------------------------- /web/public/favicons/android-chrome-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Houfeng/cize/2a2818d80dffd11d95ccca57fcc8052881207331/web/public/favicons/android-chrome-72x72.png -------------------------------------------------------------------------------- /web/public/favicons/android-chrome-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Houfeng/cize/2a2818d80dffd11d95ccca57fcc8052881207331/web/public/favicons/android-chrome-96x96.png -------------------------------------------------------------------------------- /web/public/favicons/apple-touch-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Houfeng/cize/2a2818d80dffd11d95ccca57fcc8052881207331/web/public/favicons/apple-touch-icon-114x114.png -------------------------------------------------------------------------------- /web/public/favicons/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Houfeng/cize/2a2818d80dffd11d95ccca57fcc8052881207331/web/public/favicons/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /web/public/favicons/apple-touch-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Houfeng/cize/2a2818d80dffd11d95ccca57fcc8052881207331/web/public/favicons/apple-touch-icon-144x144.png -------------------------------------------------------------------------------- /web/public/favicons/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Houfeng/cize/2a2818d80dffd11d95ccca57fcc8052881207331/web/public/favicons/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /web/public/favicons/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Houfeng/cize/2a2818d80dffd11d95ccca57fcc8052881207331/web/public/favicons/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /web/public/favicons/apple-touch-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Houfeng/cize/2a2818d80dffd11d95ccca57fcc8052881207331/web/public/favicons/apple-touch-icon-57x57.png -------------------------------------------------------------------------------- /web/public/favicons/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Houfeng/cize/2a2818d80dffd11d95ccca57fcc8052881207331/web/public/favicons/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /web/public/favicons/apple-touch-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Houfeng/cize/2a2818d80dffd11d95ccca57fcc8052881207331/web/public/favicons/apple-touch-icon-72x72.png -------------------------------------------------------------------------------- /web/public/favicons/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Houfeng/cize/2a2818d80dffd11d95ccca57fcc8052881207331/web/public/favicons/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /web/public/favicons/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Houfeng/cize/2a2818d80dffd11d95ccca57fcc8052881207331/web/public/favicons/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /web/public/favicons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Houfeng/cize/2a2818d80dffd11d95ccca57fcc8052881207331/web/public/favicons/apple-touch-icon.png -------------------------------------------------------------------------------- /web/public/favicons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | #da532c 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /web/public/favicons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Houfeng/cize/2a2818d80dffd11d95ccca57fcc8052881207331/web/public/favicons/favicon-16x16.png -------------------------------------------------------------------------------- /web/public/favicons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Houfeng/cize/2a2818d80dffd11d95ccca57fcc8052881207331/web/public/favicons/favicon-32x32.png -------------------------------------------------------------------------------- /web/public/favicons/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Houfeng/cize/2a2818d80dffd11d95ccca57fcc8052881207331/web/public/favicons/favicon-96x96.png -------------------------------------------------------------------------------- /web/public/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Houfeng/cize/2a2818d80dffd11d95ccca57fcc8052881207331/web/public/favicons/favicon.ico -------------------------------------------------------------------------------- /web/public/favicons/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cize", 3 | "icons": [ 4 | { 5 | "src": "\/android-chrome-36x36.png", 6 | "sizes": "36x36", 7 | "type": "image\/png", 8 | "density": 0.75 9 | }, 10 | { 11 | "src": "\/android-chrome-48x48.png", 12 | "sizes": "48x48", 13 | "type": "image\/png", 14 | "density": 1 15 | }, 16 | { 17 | "src": "\/android-chrome-72x72.png", 18 | "sizes": "72x72", 19 | "type": "image\/png", 20 | "density": 1.5 21 | }, 22 | { 23 | "src": "\/android-chrome-96x96.png", 24 | "sizes": "96x96", 25 | "type": "image\/png", 26 | "density": 2 27 | }, 28 | { 29 | "src": "\/android-chrome-144x144.png", 30 | "sizes": "144x144", 31 | "type": "image\/png", 32 | "density": 3 33 | }, 34 | { 35 | "src": "\/android-chrome-192x192.png", 36 | "sizes": "192x192", 37 | "type": "image\/png", 38 | "density": 4 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /web/public/favicons/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Houfeng/cize/2a2818d80dffd11d95ccca57fcc8052881207331/web/public/favicons/mstile-144x144.png -------------------------------------------------------------------------------- /web/public/favicons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Houfeng/cize/2a2818d80dffd11d95ccca57fcc8052881207331/web/public/favicons/mstile-150x150.png -------------------------------------------------------------------------------- /web/public/favicons/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Houfeng/cize/2a2818d80dffd11d95ccca57fcc8052881207331/web/public/favicons/mstile-310x150.png -------------------------------------------------------------------------------- /web/public/favicons/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Houfeng/cize/2a2818d80dffd11d95ccca57fcc8052881207331/web/public/favicons/mstile-310x310.png -------------------------------------------------------------------------------- /web/public/favicons/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Houfeng/cize/2a2818d80dffd11d95ccca57fcc8052881207331/web/public/favicons/mstile-70x70.png -------------------------------------------------------------------------------- /web/public/favicons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /web/public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Houfeng/cize/2a2818d80dffd11d95ccca57fcc8052881207331/web/public/images/logo.png -------------------------------------------------------------------------------- /web/public/js/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 公共函数 3 | **/ 4 | (function ($, $$) { 5 | 6 | $$.ajax = function (url, options, data, callback) { 7 | if (arguments.length == 3) { 8 | callback = data; 9 | data = options; 10 | options = null; 11 | } else if (arguments.length == 2) { 12 | callback = options; 13 | data = null; 14 | options = null; 15 | } 16 | options = options || {}; 17 | callback = callback || function () { }; 18 | data = data || options.data; 19 | return $.ajax({ 20 | url: url + '?_t=' + Date.now(), 21 | type: options.type || 'POST', 22 | contentType: options.contentType || "application/json", 23 | dataType: options.dataType || 'json', 24 | data: data ? JSON.stringify(data) : data, 25 | success: function (rs) { 26 | callback(null, rs); 27 | }, 28 | error: function (xhr) { 29 | callback(new Error('Ajax Error: ' + xhr.status), xhr); 30 | } 31 | }); 32 | }; 33 | 34 | $$.get = function (url, callback) { 35 | return $$.ajax(url, { 36 | type: 'GET' 37 | }, null, callback); 38 | }; 39 | 40 | $$.post = function (url, data, callback) { 41 | return $$.ajax(url, null, data, callback); 42 | }; 43 | 44 | })(jQuery, this.$$ = {}); 45 | 46 | /** 47 | * 定时刷新 48 | **/ 49 | (function ($) { 50 | 51 | var LONG_INTERVAL = 7500; 52 | var SHORT_INTERVAL = 2500; 53 | 54 | function _refresh(interval) { 55 | var console = $('.console-wraper .console'); 56 | var spinner = $('.console-wraper .fa-spinner'); 57 | var inConsole = console && console.length > 0; 58 | var isRuning = spinner && spinner.length > 0; 59 | if (!inConsole) { 60 | $('#panel-center .list-group .list-group-item.active').click(); 61 | return refresh(LONG_INTERVAL); 62 | } 63 | var scrollToButtom = function () { 64 | console.prop('scrollTop', console.prop('scrollHeight')); 65 | }; 66 | if (!isRuning) { 67 | $('.console-wraper .list-group-item').click(); 68 | return refresh(LONG_INTERVAL); 69 | } 70 | var url = '/api' + location.pathname + '/console'; 71 | $$.get(url, function (err, rs) { 72 | if (err) return console.error(err); 73 | if (console.html() != rs.out) { 74 | console.html(rs.out); 75 | scrollToButtom(); 76 | } 77 | if (rs.status != 100) { 78 | $('.console-wraper .list-group-item').click(); 79 | } 80 | return refresh(SHORT_INTERVAL); 81 | }); 82 | } 83 | function refresh(interval) { 84 | return setTimeout(_refresh, interval); 85 | } 86 | refresh(SHORT_INTERVAL); 87 | 88 | })(jQuery); 89 | 90 | /** 91 | * 手动触发 92 | **/ 93 | (function ($) { 94 | 95 | var triggerDialog = $('#trigger'); 96 | var confirmButton = $('#trigger .btn-primary'); 97 | var paramsInput = $('#trigger .params'); 98 | 99 | paramsInput.on('input', function (event) { 100 | paramsInput.removeClass('danger'); 101 | paramsInput.attr('title', ''); 102 | confirmButton.attr('title', ''); 103 | }); 104 | 105 | confirmButton.on('click', function (event) { 106 | var params = paramsInput.val() || '{}'; 107 | try { 108 | params = JSON.parse(params); 109 | } catch (err) { 110 | try { 111 | params = jsyaml.load(params); 112 | } catch (err) { 113 | paramsInput.attr('title', err.message); 114 | confirmButton.attr('title', err.message); 115 | return paramsInput.addClass('danger'); 116 | } 117 | } 118 | var triggerButton = $('#btn-trigger'); 119 | var url = triggerButton.attr('data-trigger'); 120 | $$.post(url, params, function (err) { 121 | if (err) return paramsInput.addClass('danger'); 122 | $('#panel-center .list-group .list-group-item.active').click(); 123 | triggerDialog.modal('hide'); 124 | }); 125 | }); 126 | 127 | })(jQuery); 128 | 129 | /** 130 | * 重新运行 131 | **/ 132 | (function ($) { 133 | $(document).on('click', '[data-rerun]', function (event) { 134 | var url = $(this).attr('data-rerun'); 135 | $$.post(url, function (err) { 136 | if (err) return $(this).addClass('danger');; 137 | $('#panel-center .list-group .list-group-item.active').click(); 138 | }); 139 | return false; 140 | }); 141 | })(jQuery); 142 | 143 | /** 144 | * 生成一个新的 token 145 | **/ 146 | (function ($) { 147 | var generteButton = $('#setting .generate'); 148 | var maxAgeInput = $('#setting .max-age'); 149 | var tokenArea = $('#setting .token'); 150 | 151 | maxAgeInput.on('input', function (event) { 152 | var val = maxAgeInput.val(); 153 | if (isNaN(val)) { 154 | maxAgeInput.addClass('danger'); 155 | } else { 156 | maxAgeInput.removeClass('danger'); 157 | } 158 | }); 159 | 160 | generteButton.on('click', function () { 161 | var val = maxAgeInput.val(); 162 | if (!val || isNaN(val)) { 163 | return maxAgeInput.addClass('danger'); 164 | } 165 | $$.post('/api/token', { 166 | maxAge: 60 * 60 * Number(val) 167 | }, function (err, rs) { 168 | if (err) return consoel.error(err); 169 | tokenArea.text(rs.token); 170 | }); 171 | }); 172 | 173 | })(jQuery); -------------------------------------------------------------------------------- /web/views/alert.html: -------------------------------------------------------------------------------- 1 | <% $.master("./master.html") %> 2 | 3 | <% $.placeBegin("head") %> 4 | 5 | <%= $.locale.alert() %> - <%= $.server.ci.pkg.name.toUpperCase() %> 6 | 7 | <% $.placeEnd() %> 8 | 9 | <% $.placeBegin("content") %> 10 | 11 |
12 |
13 |

14 | 15 | <%= $.locale.alert() %> 16 |

17 |
18 |
19 | <%= $.locale.noProjectsOrJobs() %> 20 |
21 |
22 | 23 | <% $.placeEnd() %> -------------------------------------------------------------------------------- /web/views/auth.html: -------------------------------------------------------------------------------- 1 | <% $.master("./master.html") %> 2 | 3 | <% $.placeBegin("head") %> 4 | 5 | <%= $.locale.auth() %> - <%= $.server.ci.pkg.name.toUpperCase() %> 6 | 7 | <% $.placeEnd() %> 8 | 9 | <% $.placeBegin("content") %> 10 | 11 |
12 |
13 |

14 | 15 | <%= $.locale.auth() %> 16 |

17 |
18 |
19 |
20 | 21 | 22 | 23 | 24 |
25 |
26 |
27 | 28 | <% $.placeEnd() %> -------------------------------------------------------------------------------- /web/views/console.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | <%= $.locale.console() %> 4 | 5 | 6 | <%= $.locale.back() %> 7 | 8 |
9 |
10 | <% var record = this.record; %> 11 |
12 |
13 |

14 | # <%= record.sn %> 15 | 16 | <% if(record.status == 300){ %> 17 | 18 | <% } %> 19 |

20 |
21 | <%= record.summary || '-' %> 22 |
23 |
24 | 25 | <%= $.formatDate(record.beginTime,'MM-dd hh:mm:ss') %> - 26 | <%= $.formatDate(record.endTime,'MM-dd hh:mm:ss') %> 27 |
28 |
29 |
30 |
<%= this.record.out %>
31 |
-------------------------------------------------------------------------------- /web/views/main.html: -------------------------------------------------------------------------------- 1 | <% $.master("./master.html") %> 2 | 3 | <% $.placeBegin("head") %> 4 | 5 | <%= $.locale.monitor() %> - <%= $.server.ci.pkg.name.toUpperCase() %> 6 | 7 | <% $.placeEnd() %> 8 | 9 | <% $.placeBegin("content") %> 10 |
11 |
12 |
13 | 14 | <%= $.locale.projects() %> 15 |
16 | 34 |
35 |
36 |
37 | 38 | <%= $.locale.jobs() %> 39 |
40 | 60 |
61 |
62 | <% $.include(this.sn?'./console.html':'./record.html') %> 63 |
64 |
65 | <% $.include('./trigger.html') %> 66 | <% $.include('./setting.html') %> 67 | 68 | <% $.placeEnd() %> 69 | 70 | <% $.placeBegin('foot') %> 71 | 72 | <% $.placeEnd() %> -------------------------------------------------------------------------------- /web/views/master.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | <% $.placeHolder('head') %> 30 | 31 | 32 | 33 | 74 |
75 | <% $.placeHolder('content') %> 76 |
77 | 78 | 79 | 80 | 81 | 84 | <% $.placeHolder('foot') %> 85 | 86 | 87 | -------------------------------------------------------------------------------- /web/views/record.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | <%= $.locale.records() %> 4 | 5 | 6 | <%= $.locale.trigger() %> 7 | 8 |
9 |
10 | <% $.each(this.records,function(index, record){ %> 11 | 12 |

13 | # <%= record.sn %> 14 | 15 | <% if(record.status == 300){ %> 16 | 17 | <% } %> 18 |

19 |
20 | <%= record.summary || '-' %> 21 |
22 |
23 | 24 | 25 | <%= $.formatDate(record.beginTime,'MM-dd hh:mm:ss') %> - 26 | <%= $.formatDate(record.endTime,'MM-dd hh:mm:ss') %> 27 |
28 |
29 | <% }.bind(this)) %> 30 |
-------------------------------------------------------------------------------- /web/views/setting.html: -------------------------------------------------------------------------------- 1 | 29 | -------------------------------------------------------------------------------- /web/views/trigger.html: -------------------------------------------------------------------------------- 1 | 22 | --------------------------------------------------------------------------------