Skip to content
This repository was archived by the owner on Jan 6, 2021. It is now read-only.

Commit 6e13985

Browse files
committed
Many updates, new version
Queue state saving Now commands writed as string instead of arrays Removed cfg.user and very simplified code because of it Changed project structure Log dir now doesn't depend of log.js location
1 parent b32e490 commit 6e13985

File tree

7 files changed

+165
-186
lines changed

7 files changed

+165
-186
lines changed

README.md

+11-16
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,37 @@
11
About
22
===
33
Git web-hook receiver written in node.js.
4-
Created for prevent a routine work after commit. Useful if you have a server for deploy and when using git :-)
4+
Created for prevent a routine work after commit. Useful if you have a server for deploy and using git :-)
55

66
Requirements
77
===
88
* nodejs >= v0.6.21
9-
* System: linux, *bsd(tested on Ubuntu)
9+
* System: Linux(tested on Ubuntu), *BSD(Not tested)
1010

1111
Instalation
1212
===
1313
```
1414
git clone git://github.com/sashasimkin/hook-receiver.git
1515
```
16-
17-
Usage
18-
===
19-
1. Create file {{name}}.json with config object in dir config/ below about this).
20-
2. node app.js IP:PORT (IP non-required, but if you filled port only the command must looks like `node app.js :PORT`). Defaults `IP='0.0.0.0';PORT=8001`.
21-
3. Use `http://IP:PORT/{{name}}` as hook url in github, gitlab, etc.
22-
4. Set chmod 0777 on logs/ directory
16+
1. `cd hook-reciever/`
17+
2. `chmod 0777 logs/`
18+
2. Create file `{name}.json` with config object in directory `config/` (Below about its contents).
19+
3. run server `node app.js IP:PORT` (IP non-required, but if you filled port only the command must looks like `node app.js :PORT`). Defaults `IP="0.0.0.0";PORT=8001`.
20+
4. Use `http://IP:PORT/{name}` as hook url in github, gitlab, etc.
2321

2422
Configuration:
2523
===
2624
It is a json file with json object inside in directory config/. This configuration used every time when hook has been recieved.
2725

2826
Configuration parameters:
2927
===
30-
* `user` - System user, from which will be performed commands
31-
* `path` - root path for project, shell commans has been executed there
32-
* `commands` - Shell commands, which will be performed after recieve hook
33-
* `refs` - Non-required, if ref not match, any operation will not be performed. string or array of strings which be substituted to ref.match()
28+
* `path` - Root path for project, shell commands has been executed here
29+
* `commands` - Array of shell commands, which will be performed after recieve hook
30+
* `refs` - Commits ref to match. String or array of strings which will be substituted to ref.match()
3431

3532

3633
TODO
3734
===
38-
* Queue state persistence
39-
* Get user id in system (id -u {cfg.user})
4035
* Variables in command definition
4136
* Commands string instade of array
42-
* cfg.require parameter with applying the cfg.commands results
37+
* cfg.require parameter - such as cfg.commands, but javascript files for inclusion

app.js

+75-92
Original file line numberDiff line numberDiff line change
@@ -2,94 +2,90 @@
22
var http = require("http");
33
var fs = require("fs");
44
var path = require("path");
5-
var child_process = require('child_process');
6-
var log = require("./lib/logging");
7-
var TaskManager = require("./lib/tasks/TaskManager");
8-
//Runtime variables
9-
var cfg_dir = __dirname + '/config/';
10-
var cfg_map = {};
5+
var exec = require('child_process').exec;
6+
var log = require("./lib/log");
7+
var TaskManager = require("./lib/TaskManager");
118

12-
//Server options
9+
//IP and PORT for web-server
1310
var run_argv = process.argv[2] ? process.argv[2].split(':') : [];
1411
process.env.IP = process.env.IP || run_argv[0] || '0.0.0.0';
1512
process.env.PORT = process.env.PORT || run_argv[1] || 8001;
1613

17-
var processFile = function (fileName) {
18-
var filePath = cfg_dir + fileName;
19-
var fileExt = path.extname(fileName);
20-
if (fileExt != '.json') {
21-
return log('Problem with file "' + filePath + '". There must be .json file extension.', 'runtime');
22-
}
23-
24-
fs.readFile(filePath, 'utf-8', function (err, data) {
25-
var cfg = {};
26-
try {
27-
if (err) throw err;
14+
//Runtime variables
15+
var cfg = {
16+
dir: __dirname + '/config/',
17+
map: {}
18+
};
2819

29-
cfg = JSON.parse(data);
30-
if ([cfg.path, cfg.user, cfg.commands].indexOf(undefined) !== -1) {
31-
throw new Error('Bad config file "' + filePath + '". It need to be json object with path, user and commands keys.');
32-
}
33-
} catch (e) {
34-
return log('Error while processing file "' + filePath + '": ' + e, 'runtime');
20+
/**
21+
* Validate file and add to cfg map if valid
22+
*/
23+
var processFile = function (fName) {
24+
var fPath = path.join(cfg.dir, fName),
25+
fExt = path.extname(fName);
26+
27+
if (fExt != '.json') return log('Problem with file "' + fPath + '". There must be .json file extension.', 'runtime');
28+
29+
try{
30+
delete require.cache[fPath];
31+
var data = require(fPath);
32+
33+
if (!(data.path && typeof data.commands == 'object' && data.commands.length)) {
34+
throw new Error('Bad config file "' + fPath + '". It need to be json object with path and commands keys.');
3535
}
36-
//Populate good cfg object to objects map by filename without extension
37-
return cfg_map[path.basename(fileName, fileExt)] = cfg;
38-
});
36+
37+
fs.readdirSync(data.path);
38+
39+
return cfg.map[path.basename(fName, fExt)] = data;
40+
} catch(e) {
41+
return log('Error while processing file "' + fPath + '": ' + e, 'runtime');
42+
}
3943
};
4044

41-
// Readfiles to object on server start
42-
fs.readdir(cfg_dir, function (wtf, files) {
43-
var watchCallback = function (prev, next) {
44-
processFile(files[i]);
45+
/**
46+
* Callback for file-watchers
47+
*/
48+
var watchCallback = function(file) {
49+
processFile(file);
50+
return function (curr, prev) {
51+
if(prev.mtime != curr.mtime) processFile(file);
4552
};
46-
47-
for (var i in files) {
53+
};
54+
//Readfiles to object on app start
55+
fs.readdir(cfg.dir, function (err, files) {
56+
if(err) return log(err, 'startup');
57+
58+
files.map(function(file, i) {
4859
try {
49-
processFile(files[i]);
50-
fs.watchFile(cfg_dir + files[i], watchCallback);
60+
fs.watchFile(path.join(cfg.dir, file), watchCallback(file));
5161
} catch (e) {
5262
log(e, 'startup');
5363
}
54-
}
55-
56-
//Watch for changes
57-
fs.watch(cfg_dir, function (event, fileName) {
58-
processFile(fileName);
5964
});
6065
});
6166

67+
//Watch for changes in directory
68+
fs.watch(cfg.dir, function (event, fName) {
69+
processFile(fName);
70+
});
71+
6272
var runTask = function (task) {
63-
task.started();
64-
var cmd = task.commands.shift();
65-
if (!cmd) {
66-
return task.stopped();
73+
task.running();
74+
var command = task.commands.shift();
75+
if (!command) {
76+
return task.finished();
6777
}
68-
69-
var proc = child_process.spawn(cmd.shift(), cmd, task.options),
70-
cmd_string = cmd.join(' '),
71-
stdout = '',
72-
stderror = '';
73-
74-
proc.stdout.on('data', function (data) {
75-
stdout += data;
76-
});
77-
proc.stderr.on('data', function (data) {
78-
stderror += data;
79-
});
80-
proc.on('exit', function (code, signal) {
81-
//Log results of current command
82-
if (stdout) log('Data from "' + cmd_string + '": ' + stdout, task.name + '.info');
83-
if (stderror) log('Errors in "' + cmd_string + '": ' + stderror, task.name + '.error');
84-
//Run next task, pass reference to current task
78+
79+
exec(command, task.options, function(err, stdout, stderr) {
80+
if (stdout) log('Data from "' + command + '": ' + stdout, task.name + '.info');
81+
if (stderr) log('Error in "' + command + '": ' + stderr, task.name + '.error');
82+
8583
runTask(task);
8684
});
8785
};
8886

89-
//Get initial variables, or move it inside class
90-
var initial = {};
91-
var queue = new TaskManager(initial);
92-
87+
//Setup task manager
88+
var queue = new TaskManager();
9389
queue.run(function () {
9490
runTask(this);
9591
}, 1000);
@@ -99,9 +95,9 @@ http.createServer(function (request, response) {
9995
//Strip request.url, remove first slash
10096
request.url = request.url.slice(1);
10197
//Prevent favicon.ico requests
102-
if (request.url == 'favicon.ico') return request.connection.destroy();
98+
if (request.methom == 'GET' && request.url == 'favicon.ico') return request.connection.destroy();
10399

104-
if (request.method == 'POST' && cfg_map[request.url]) {
100+
if (request.method == 'POST' && cfg.map[request.url]) {
105101
var body = '';
106102
request.on('data', function (data) {
107103
body += data;
@@ -117,42 +113,29 @@ http.createServer(function (request, response) {
117113
} catch (e) {
118114
return log('Malformed json. Request body: ' + body, request.url + '.error');
119115
}
120-
121116
//We need object copy!
122-
var cfg = JSON.parse(JSON.stringify(cfg_map[request.url]));
123-
var spawn_options = {
117+
var task = JSON.parse(JSON.stringify(cfg.map[request.url]));
118+
task.name = request.url;
119+
task.options = {
124120
encoding: "utf-8",
125-
env: process.env
121+
env: process.env,
122+
cwd: task.path
126123
};
127124

128-
if (cfg.user) {
129-
spawn_options.uid = cfg.user;
130-
}
131-
132-
try {
133-
fs.readdirSync(cfg.path);
134-
spawn_options.cwd = cfg.path;
135-
} catch (e) {
136-
return log('Invalid path "' + cfg.path + '" in config "' + request.url + '"', request.url + '.error');
137-
}
138-
139-
if (cfg.refs) {
140-
var refsType = typeof cfg.refs;
125+
if (task.refs) {
126+
var refsType = typeof task.refs;
141127
if (['string', 'object'].indexOf(refsType)) {
142-
if (refsType == 'string') cfg.refs = [cfg.refs];
128+
if (refsType == 'string') task.refs = [task.refs];
143129

144-
var suitableRef = Object.keys(cfg.refs).some(function (k) {
145-
return bodyObj.ref.match(cfg.refs[k]);
130+
var suitableRef = Object.keys(task.refs).some(function (k) {
131+
return bodyObj.ref.match(task.refs[k]);
146132
});
147133
if (!suitableRef) return log('Ref does not fit. Aborting.', request.url + '.info');
148134
}
149135
}
150136

151-
if (cfg.commands.length) {
152-
cfg.name = request.url;
153-
cfg.options = spawn_options;
154-
155-
queue.push(request.url, cfg);
137+
if (task.commands.length) {
138+
queue.push(request.url, task);
156139
} else {
157140
return log('No commands to execute.', request.url + '.info');
158141
}

config/sample.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
"refs": [
44
"refs/heads/*"
55
],
6-
"user": "shell-user",
76
"commands": [
8-
["git", "fetch", "--all"],
9-
["git", "reset", "--hard", "origin/master"]
7+
"git clone git://github.com/sashasimkin/hook-receiver.git",
8+
"git fetch --all",
9+
"git reset --hard origin/master"
1010
]
1111
}

lib/TaskManager.js

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
var os = require("os"),
2+
path = require("path"),
3+
fs = require("fs");
4+
5+
exports = module.exports = function() {
6+
var self = this;
7+
var queue = {};
8+
var state_file = path.join(os.tmpdir(), 'hook-reciever-state.json');
9+
10+
//Custom not enumerable length for counting all tasks in queue
11+
Object.defineProperty(queue, "length", {
12+
writable: true,
13+
value: 0
14+
});
15+
16+
self.saveState = function() {
17+
if(queue.length){
18+
fs.writeFile(state_file, JSON.stringify(queue), function(err) {});
19+
}
20+
};
21+
22+
self.get = function() {
23+
if(queue.length) {
24+
for(var type in queue){
25+
if(queue[type].tasks.length && !queue[type].running){
26+
queue.length--;
27+
return queue[type].tasks.shift();
28+
}
29+
}
30+
}
31+
//If nothing
32+
return null;
33+
};
34+
35+
//self.push(request.url, cfg)
36+
self.push = function(type, task) {
37+
queue.length++;
38+
if(!queue[type]) queue[type] = { running: false };
39+
if(!queue[type].tasks) queue[type].tasks = [];
40+
41+
task.running = function() {
42+
return (queue[type].running = true);
43+
};
44+
task.finished = function() {
45+
return (queue[type].running = false);
46+
};
47+
48+
queue[type].tasks.push(task);
49+
};
50+
51+
//Add try-catch, or not.
52+
self.run = function(callback, delay) {
53+
return setInterval(function() {
54+
var task = self.get();
55+
if(task){
56+
self.saveState();
57+
callback.apply(task);
58+
}
59+
}, delay);
60+
};
61+
62+
self.stop = function(id){
63+
return clearInterval(id);
64+
};
65+
66+
try{
67+
//It maybe alredy included, I dont know how. In any case does not prevent
68+
delete require.cache[state_file];
69+
queue = require(state_file);
70+
//Mark all tasks as stopped, if we fall when some task executing
71+
for(var k in queue) queue[k].running = false;
72+
} catch(e) {}
73+
74+
return self;
75+
};

lib/logging/log.js lib/log.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ var fs = require("fs");
22
var path = require("path");
33

44
var dbg = true;
5-
var logs_dir = __dirname + '/../../logs';
5+
var logs_dir = path.join(process.cwd(), 'logs');
66

77
Date.prototype.toLocaleFormat = function(format) {
88
var f = {y : this.getYear() + 1900,m : this.getMonth() + 1,d : this.getDate(),H : this.getHours(),M : this.getMinutes(),s : this.getSeconds()};

lib/logging/package.json

-21
This file was deleted.

0 commit comments

Comments
 (0)