1 |
|
2 | (function() {
|
3 | var Crossover, cluster, domain, express, fs, os, rest, spawn, temp, util, uuid, wrench,
|
4 | __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
|
5 |
|
6 | cluster = require("cluster");
|
7 |
|
8 | domain = require("domain");
|
9 |
|
10 | express = require("express");
|
11 |
|
12 | fs = require("fs");
|
13 |
|
14 | os = require("os");
|
15 |
|
16 | rest = require("restler");
|
17 |
|
18 | spawn = require("child_process").spawn;
|
19 |
|
20 | temp = require("temp");
|
21 |
|
22 | util = require("util");
|
23 |
|
24 | uuid = require("node-uuid");
|
25 |
|
26 | wrench = require("wrench");
|
27 |
|
28 | module.exports.version = require("../package.json").version;
|
29 |
|
30 | Crossover = (function() {
|
31 |
|
32 | function Crossover(options) {
|
33 | this.options = options;
|
34 | this.execute = __bind(this.execute, this);
|
35 |
|
36 | this.slave = __bind(this.slave, this);
|
37 |
|
38 | this.master = __bind(this.master, this);
|
39 |
|
40 | this.listen = __bind(this.listen, this);
|
41 |
|
42 | this.test_worker = __bind(this.test_worker, this);
|
43 |
|
44 | this.spawn_worker = __bind(this.spawn_worker, this);
|
45 |
|
46 | this.prepare_npm = __bind(this.prepare_npm, this);
|
47 |
|
48 | this.prepare_worker = __bind(this.prepare_worker, this);
|
49 |
|
50 | this.app = null;
|
51 | this.listening = false;
|
52 | this.stopping = false;
|
53 | this.workers = [];
|
54 | this.root = temp.mkdirSync("crossover");
|
55 | }
|
56 |
|
57 | Crossover.prototype.prepare_worker = function(slug, env, cb) {
|
58 | var target,
|
59 | _this = this;
|
60 | target = this.root + "/" + uuid.v1();
|
61 | this.log("preparing worker: " + slug);
|
62 | return this.read_env(env, function(env) {
|
63 | if (slug.substring(0, 4) === "http") {
|
64 | return rest.get(slug, {
|
65 | decoding: "buffer"
|
66 | }).on("complete", function(result) {
|
67 | return fs.mkdir(target, function(err) {
|
68 | return fs.writeFile(target + "/app.tgz", result, "binary", function(err) {
|
69 | return _this.execute("tar", ["xzf", "app.tgz"], {
|
70 | cwd: target
|
71 | }, function() {
|
72 | return _this.prepare_npm(target, function(target) {
|
73 | return cb(target, env);
|
74 | });
|
75 | });
|
76 | });
|
77 | });
|
78 | });
|
79 | } else {
|
80 | wrench.copyDirSyncRecursive(slug, target);
|
81 | return _this.prepare_npm(target, function(target) {
|
82 | return cb(target, env);
|
83 | });
|
84 | }
|
85 | });
|
86 | };
|
87 |
|
88 | Crossover.prototype.read_env = function(env, cb) {
|
89 | var _this = this;
|
90 | if (!env) {
|
91 | return cb({});
|
92 | } else if (env.substring(0, 4) === "http") {
|
93 | return rest.get(env).on("complete", function(result) {
|
94 | return cb(_this.read_env_data(result));
|
95 | });
|
96 | } else {
|
97 | return fs.readFile(env, function(err, data) {
|
98 | return cb(_this.read_env_data(data.toString()));
|
99 | });
|
100 | }
|
101 | };
|
102 |
|
103 | Crossover.prototype.read_env_data = function(data) {
|
104 | var env, line, parts, _i, _len, _ref;
|
105 | env = {};
|
106 | _ref = data.split("\n");
|
107 | for (_i = 0, _len = _ref.length; _i < _len; _i++) {
|
108 | line = _ref[_i];
|
109 | parts = line.split("=");
|
110 | env[parts.shift()] = parts.join("=");
|
111 | }
|
112 | return env;
|
113 | };
|
114 |
|
115 | Crossover.prototype.prepare_npm = function(target, cb) {
|
116 | var _this = this;
|
117 | this.log("resolving dependencies");
|
118 | return this.execute("npm", ["install"], {
|
119 | cwd: target
|
120 | }, function() {
|
121 | return _this.execute("npm", ["rebuild"], {
|
122 | cwd: target
|
123 | }, function() {
|
124 | return cb(target);
|
125 | });
|
126 | });
|
127 | };
|
128 |
|
129 | Crossover.prototype.spawn_worker = function(dir, cb) {
|
130 | var old_cwd, old_env, worker;
|
131 | old_env = process.env;
|
132 | old_cwd = process.cwd();
|
133 | process.env = this.env || {};
|
134 | process.chdir(dir);
|
135 | worker = cluster.fork();
|
136 | process.chdir(old_cwd);
|
137 | process.env = old_env;
|
138 | this.log("forked worker " + worker.process.pid);
|
139 | return worker.on("message", function(msg) {
|
140 | if (msg.cmd === "ready") {
|
141 | this.send({
|
142 | cmd: "start",
|
143 | dir: dir
|
144 | });
|
145 | if (cb) {
|
146 | return cb(this);
|
147 | }
|
148 | }
|
149 | });
|
150 | };
|
151 |
|
152 | Crossover.prototype.test_worker = function(dir, env, cb) {
|
153 | var old_cwd, old_env, worker;
|
154 | old_env = process.env;
|
155 | old_cwd = process.cwd();
|
156 | process.env = this.env || {};
|
157 | process.chdir(dir);
|
158 | worker = cluster.fork();
|
159 | process.chdir(old_cwd);
|
160 | process.env = old_env;
|
161 | this.log("forked worker " + worker.process.pid);
|
162 | return worker.on("message", function(msg) {
|
163 | if (msg.cmd === "ready") {
|
164 | return this.send({
|
165 | cmd: "test",
|
166 | dir: dir
|
167 | });
|
168 | } else if (msg.cmd === "success") {
|
169 | return cb(null);
|
170 | } else if (msg.cmd === "failure") {
|
171 | return cb(msg.err);
|
172 | }
|
173 | });
|
174 | };
|
175 |
|
176 | Crossover.prototype.listen = function(slug, env, port) {
|
177 | var _this = this;
|
178 | if (!slug) {
|
179 | this.error("Must specify a slug.");
|
180 | }
|
181 | if (cluster.isMaster) {
|
182 | this.admin().listen(this.options["managementPort"], function() {
|
183 | return console.log("[master] listening on management port: " + _this.options["managementPort"]);
|
184 | });
|
185 | return this.prepare_worker(slug, env, function(slug, env) {
|
186 | _this.slug = slug;
|
187 | _this.env = env;
|
188 | return _this.master();
|
189 | });
|
190 | } else {
|
191 | return this.slave(port);
|
192 | }
|
193 | };
|
194 |
|
195 | Crossover.prototype.master = function() {
|
196 | var num, _i, _ref,
|
197 | _this = this;
|
198 | for (num = _i = 1, _ref = this.options.concurrency; 1 <= _ref ? _i <= _ref : _i >= _ref; num = 1 <= _ref ? ++_i : --_i) {
|
199 | this.spawn_worker(this.slug, function(worker) {
|
200 | return _this.workers.push(worker);
|
201 | });
|
202 | }
|
203 | return cluster.on("exit", function(worker) {
|
204 | _this.log("worker " + worker.pid + " died");
|
205 | _this.workers.splice(_this.workers.indexOf(worker), 1);
|
206 | return _this.spawn_worker(_this.slug, function(worker) {
|
207 | return _this.workers.push(worker);
|
208 | });
|
209 | });
|
210 | };
|
211 |
|
212 | Crossover.prototype.slave = function(port) {
|
213 | var _this = this;
|
214 | process.on("message", function(msg) {
|
215 | switch (msg.cmd) {
|
216 | case "start":
|
217 | _this.log("starting app");
|
218 | _this.listening = false;
|
219 | _this.app = require(msg.dir + "/index");
|
220 | _this.app.on("close", function() {
|
221 | _this.log("requests completed, exiting");
|
222 | return process.exit(0);
|
223 | });
|
224 | return _this.app.listen(port, function() {
|
225 | _this.log("listening on port: " + port);
|
226 | return _this.listening = true;
|
227 | });
|
228 | case "test":
|
229 | _this.log("launching test app from slug");
|
230 | try {
|
231 | _this.app = require(msg.dir + "/index");
|
232 | return _this.app.listen(0, function() {
|
233 | return process.send({
|
234 | cmd: "success"
|
235 | });
|
236 | });
|
237 | } catch (err) {
|
238 | return process.send({
|
239 | cmd: "failure",
|
240 | err: err.toString()
|
241 | });
|
242 | }
|
243 | break;
|
244 | case "stop":
|
245 | if (!_this.stopping) {
|
246 | _this.stopping = true;
|
247 | if (_this.listening) {
|
248 | _this.log("turning off new connections to app");
|
249 | _this.app.close();
|
250 | return setTimeout((function() {
|
251 | _this.log("giving up on remaining connections");
|
252 | return process.exit(0);
|
253 | }), 30000);
|
254 | } else {
|
255 | _this.log("app not listening yet, exiting");
|
256 | return process.exit(0);
|
257 | }
|
258 | }
|
259 | }
|
260 | });
|
261 | return process.send({
|
262 | cmd: "ready"
|
263 | });
|
264 | };
|
265 |
|
266 | Crossover.prototype.admin = function() {
|
267 | var admin,
|
268 | _this = this;
|
269 | admin = require("express").createServer(express.bodyParser(), express.basicAuth("admin", (this.options['auth'] || "").toString()));
|
270 | admin.get("/status", function(req, res) {
|
271 | res.contentType("application/json");
|
272 | return res.send(JSON.stringify({
|
273 | version: module.exports.version
|
274 | }));
|
275 | });
|
276 | admin.post("/release", function(req, res) {
|
277 | var dom;
|
278 | dom = domain.create();
|
279 | dom.on("error", function(err) {
|
280 | _this.log("failed to launch: " + err);
|
281 | res.writeHead(403);
|
282 | return res.end("error");
|
283 | });
|
284 | return dom.run(function() {
|
285 | var env, slug;
|
286 | slug = req.body.slug;
|
287 | env = req.body.env;
|
288 | _this.log("releasing: " + slug + " " + env);
|
289 | return _this.prepare_worker(slug, env, function(slug, env) {
|
290 | return _this.test_worker(slug, env, function(err) {
|
291 | var worker, _i, _len, _ref;
|
292 | if (err) {
|
293 | _this.log("error in slug, aborting spawn: " + err);
|
294 | res.writeHead(403);
|
295 | return res.end("error");
|
296 | } else {
|
297 | _this.log("test successful");
|
298 | _this.slug = slug;
|
299 | _this.env = env;
|
300 | _ref = _this.workers;
|
301 | for (_i = 0, _len = _ref.length; _i < _len; _i++) {
|
302 | worker = _ref[_i];
|
303 | worker.send({
|
304 | cmd: "stop"
|
305 | });
|
306 | }
|
307 | return res.send("ok");
|
308 | }
|
309 | });
|
310 | });
|
311 | });
|
312 | });
|
313 | return admin;
|
314 | };
|
315 |
|
316 | Crossover.prototype.format_log = function(args) {
|
317 | var arg, formatted, pid, _i, _len;
|
318 | pid = cluster.isMaster ? "master" : "worker:" + process.pid;
|
319 | formatted = ["[" + pid + "]"];
|
320 | for (_i = 0, _len = args.length; _i < _len; _i++) {
|
321 | arg = args[_i];
|
322 | formatted.push(arg);
|
323 | }
|
324 | return formatted;
|
325 | };
|
326 |
|
327 | Crossover.prototype.log = function() {
|
328 | return console.log.apply(console, this.format_log(arguments));
|
329 | };
|
330 |
|
331 | Crossover.prototype.error = function() {
|
332 | var arg, args, _i, _len;
|
333 | args = ["[" + process.pid + "]"];
|
334 | for (_i = 0, _len = arguments.length; _i < _len; _i++) {
|
335 | arg = arguments[_i];
|
336 | args.push(arg);
|
337 | }
|
338 | console.error.apply(console, args);
|
339 | return process.exit(1);
|
340 | };
|
341 |
|
342 | Crossover.prototype.execute = function(command, args, options, cb) {
|
343 | var child,
|
344 | _this = this;
|
345 | child = spawn(command, args, options);
|
346 | child.stdout.on("data", function(data) {
|
347 | var part, _i, _len, _ref, _results;
|
348 | _ref = data.toString().replace(/\n$/, '').split("\n");
|
349 | _results = [];
|
350 | for (_i = 0, _len = _ref.length; _i < _len; _i++) {
|
351 | part = _ref[_i];
|
352 | if (process.env.DEBUG) {
|
353 | _results.push(console.log("[compiler] " + part));
|
354 | } else {
|
355 | _results.push(void 0);
|
356 | }
|
357 | }
|
358 | return _results;
|
359 | });
|
360 | child.stderr.on("data", function(data) {
|
361 | var part, _i, _len, _ref, _results;
|
362 | _ref = data.toString().replace(/\n$/, '').split("\n");
|
363 | _results = [];
|
364 | for (_i = 0, _len = _ref.length; _i < _len; _i++) {
|
365 | part = _ref[_i];
|
366 | if (process.env.DEBUG) {
|
367 | _results.push(console.log("[compiler] " + part));
|
368 | } else {
|
369 | _results.push(void 0);
|
370 | }
|
371 | }
|
372 | return _results;
|
373 | });
|
374 | return child.on("exit", function(code) {
|
375 | return cb(code);
|
376 | });
|
377 | };
|
378 |
|
379 | return Crossover;
|
380 |
|
381 | })();
|
382 |
|
383 | module.exports.create = function(slug) {
|
384 | return new Crossover(slug);
|
385 | };
|
386 |
|
387 | }).call(this);
|