├── .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 | [](http://badge.fury.io/js/cize)
7 | [](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 |
<%= this.record.out %>31 |