UNPKG

24.5 kBPlain TextView Raw
1#!/usr/bin/env node
2
3/**
4 * Module dependencies.
5 */
6var fs = require('fs'),
7 os = require('os'),
8 path = require('path'),
9 util = require('util'),
10 cliff = require('cliff'),
11 mkdirp = require('mkdirp'),
12 co = require('../lib/modules/console'),
13 utils = require('../lib/util/utils'),
14 starter = require('../lib/master/starter'),
15 exec = require('child_process').exec,
16 spawn = require('child_process').spawn,
17 version = require('../package.json').version,
18 adminClient = require('myhero-admin').adminClient,
19 constants = require('../lib/util/constants'),
20 program = require('commander');
21
22/**
23 * Constant Variables
24 */
25var TIME_INIT = 1 * 1000;
26var TIME_KILL_WAIT = 5 * 1000;
27var KILL_CMD_LUX = 'kill -9 `ps -ef|grep node|awk \'{print $2}\'`';
28var KILL_CMD_WIN = 'taskkill /im node.exe /f';
29
30var CUR_DIR = process.cwd();
31var DEFAULT_GAME_SERVER_DIR = CUR_DIR;
32var DEFAULT_USERNAME = 'admin';
33var DEFAULT_PWD = 'admin';
34var DEFAULT_ENV = 'development';
35var DEFAULT_MASTER_HOST = '127.0.0.1';
36var DEFAULT_MASTER_PORT = 3005;
37
38var CONNECT_ERROR = 'Fail to connect to admin console server.';
39var FILEREAD_ERROR = 'Fail to read the file, please check if the application is started legally.';
40var CLOSEAPP_INFO = 'Closing the application......\nPlease wait......';
41var ADD_SERVER_INFO = 'Successfully add server.';
42var RESTART_SERVER_INFO = 'Successfully restart server.';
43var INIT_PROJ_NOTICE = '\nThe default admin user is: \n\n' + ' username'.green + ': admin\n ' + 'password'.green + ': admin\n\nYou can configure admin users by editing adminUser.json later.\n ';
44var SCRIPT_NOT_FOUND = 'Fail to find an appropriate script to run,\nplease check the current work directory or the directory specified by option `--directory`.\n'.red;
45var MASTER_HA_NOT_FOUND = 'Fail to find an appropriate masterha config file, \nplease check the current work directory or the arguments passed to.\n'.red;
46var COMMAND_ERROR = 'Illegal command format. Use `myhero --help` to get more info.\n'.red;
47var DAEMON_INFO = 'The application is running in the background now.\n';
48
49program.version(version);
50
51program.command('init [path]')
52 .description('create a new application')
53 .action(function (path) {
54 init(path || CUR_DIR);
55 });
56
57program.command('start')
58 .description('start the application')
59 .option('-e, --env <env>', 'the used environment', DEFAULT_ENV)
60 .option('-D, --daemon', 'enable the daemon start')
61 .option('-d, --directory, <directory>', 'the code directory', DEFAULT_GAME_SERVER_DIR)
62 .option('-t, --type <server-type>,', 'start server type')
63 .option('-i, --id <server-id>', 'start server id')
64 .action(function (opts) {
65 start(opts);
66 });
67
68program.command('list')
69 .description('list the servers')
70 .option('-u, --username <username>', 'administration user name', DEFAULT_USERNAME)
71 .option('-p, --password <password>', 'administration password', DEFAULT_PWD)
72 .option('-h, --host <master-host>', 'master server host', DEFAULT_MASTER_HOST)
73 .option('-P, --port <master-port>', 'master server port', DEFAULT_MASTER_PORT)
74 .action(function (opts) {
75 list(opts);
76 });
77
78program.command('add')
79 .description('add a new server')
80 .option('-u, --username <username>', 'administration user name', DEFAULT_USERNAME)
81 .option('-p, --password <password>', 'administration password', DEFAULT_PWD)
82 .option('-h, --host <master-host>', 'master server host', DEFAULT_MASTER_HOST)
83 .option('-P, --port <master-port>', 'master server port', DEFAULT_MASTER_PORT)
84 .action(function () {
85 var args = [].slice.call(arguments, 0);
86 var opts = args[args.length - 1];
87 opts.args = args.slice(0, -1);
88 add(opts);
89 });
90
91program.command('stop')
92 .description('stop the servers, for multiple servers, use `myhero stop server-id-1 server-id-2`')
93 .option('-u, --username <username>', 'administration user name', DEFAULT_USERNAME)
94 .option('-p, --password <password>', 'administration password', DEFAULT_PWD)
95 .option('-h, --host <master-host>', 'master server host', DEFAULT_MASTER_HOST)
96 .option('-P, --port <master-port>', 'master server port', DEFAULT_MASTER_PORT)
97 .action(function () {
98 var args = [].slice.call(arguments, 0);
99 var opts = args[args.length - 1];
100 opts.serverIds = args.slice(0, -1);
101 terminal('stop', opts);
102 });
103
104program.command('kill')
105 .description('kill the application')
106 .option('-u, --username <username>', 'administration user name', DEFAULT_USERNAME)
107 .option('-p, --password <password>', 'administration password', DEFAULT_PWD)
108 .option('-h, --host <master-host>', 'master server host', DEFAULT_MASTER_HOST)
109 .option('-P, --port <master-port>', 'master server port', DEFAULT_MASTER_PORT)
110 .option('-f, --force', 'using this option would kill all the node processes')
111 .action(function () {
112 var args = [].slice.call(arguments, 0);
113 var opts = args[args.length - 1];
114 opts.serverIds = args.slice(0, -1);
115 terminal('kill', opts);
116 });
117
118program.command('restart')
119 .description('restart the servers, for multiple servers, use `myhero restart server-id-1 server-id-2`')
120 .option('-u, --username <username>', 'administration user name', DEFAULT_USERNAME)
121 .option('-p, --password <password>', 'administration password', DEFAULT_PWD)
122 .option('-h, --host <master-host>', 'master server host', DEFAULT_MASTER_HOST)
123 .option('-P, --port <master-port>', 'master server port', DEFAULT_MASTER_PORT)
124 .option('-t, --type <server-type>,', 'start server type')
125 .option('-i, --id <server-id>', 'start server id')
126 .action(function (opts) {
127 restart(opts);
128 });
129
130program.command('masterha')
131 .description('start all the slaves of the master')
132 .option('-d, --directory <directory>', 'the code directory', DEFAULT_GAME_SERVER_DIR)
133 .action(function (opts) {
134 startMasterha(opts);
135 });
136
137program.command('*')
138 .action(function () {
139 console.log(COMMAND_ERROR);
140 });
141
142program.parse(process.argv);
143
144/**
145 * Init application at the given directory `path`.
146 *
147 * @param {String} path
148 */
149function init(path) {
150 console.log(INIT_PROJ_NOTICE);
151 connectorType(function (type) {
152 emptyDirectory(path, function (empty) {
153 if (empty) {
154 process.stdin.destroy();
155 createApplicationAt(path, type);
156 } else {
157 confirm('Destination is not empty, continue? (y/n) [no] ', function (force) {
158 process.stdin.destroy();
159 if (force) {
160 createApplicationAt(path, type);
161 } else {
162 abort('Fail to init a project'.red);
163 }
164 });
165 }
166 });
167 });
168}
169
170/**
171 * Create directory and files at the given directory `path`.
172 *
173 * @param {String} ph
174 */
175function createApplicationAt(ph, type) {
176 var name = path.basename(path.resolve(CUR_DIR, ph));
177 copy(path.join(__dirname, '../template/'), ph);
178 mkdir(path.join(ph, 'game-server/logs'));
179 mkdir(path.join(ph, 'shared'));
180 // rmdir -r
181 var rmdir = function (dir) {
182 var list = fs.readdirSync(dir);
183 for (var i = 0; i < list.length; i++) {
184 var filename = path.join(dir, list[i]);
185 var stat = fs.statSync(filename);
186 if (filename === "." || filename === "..") {
187 } else if (stat.isDirectory()) {
188 rmdir(filename);
189 } else {
190 fs.unlinkSync(filename);
191 }
192 }
193 fs.rmdirSync(dir);
194 };
195 setTimeout(function () {
196 switch (type) {
197 case '1':
198 // use websocket
199 var unlinkFiles = ['game-server/app.js.sio',
200 'game-server/app.js.wss',
201 'game-server/app.js.mqtt',
202 'game-server/app.js.sio.wss',
203 'game-server/app.js.udp',
204 'web-server/app.js.https',
205 'web-server/public/index.html.sio',
206 'web-server/public/js/lib/pomeloclient.js',
207 'web-server/public/js/lib/pomeloclient.js.wss',
208 'web-server/public/js/lib/build/build.js.wss',
209 'web-server/public/js/lib/socket.io.js'];
210 for (var i = 0; i < unlinkFiles.length; ++i) {
211 fs.unlinkSync(path.resolve(ph, unlinkFiles[i]));
212 }
213 break;
214 case '2':
215 // use socket.io
216 var unlinkFiles = ['game-server/app.js',
217 'game-server/app.js.wss',
218 'game-server/app.js.udp',
219 'game-server/app.js.mqtt',
220 'game-server/app.js.sio.wss',
221 'web-server/app.js.https',
222 'web-server/public/index.html',
223 'web-server/public/js/lib/component.json',
224 'web-server/public/js/lib/pomeloclient.js.wss'];
225 for (var i = 0; i < unlinkFiles.length; ++i) {
226 fs.unlinkSync(path.resolve(ph, unlinkFiles[i]));
227 }
228
229 fs.renameSync(path.resolve(ph, 'game-server/app.js.sio'), path.resolve(ph, 'game-server/app.js'));
230 fs.renameSync(path.resolve(ph, 'web-server/public/index.html.sio'), path.resolve(ph, 'web-server/public/index.html'));
231
232 rmdir(path.resolve(ph, 'web-server/public/js/lib/build'));
233 rmdir(path.resolve(ph, 'web-server/public/js/lib/local'));
234 break;
235 case '3':
236 // use websocket wss
237 var unlinkFiles = ['game-server/app.js.sio',
238 'game-server/app.js',
239 'game-server/app.js.udp',
240 'game-server/app.js.sio.wss',
241 'game-server/app.js.mqtt',
242 'web-server/app.js',
243 'web-server/public/index.html.sio',
244 'web-server/public/js/lib/pomeloclient.js',
245 'web-server/public/js/lib/pomeloclient.js.wss',
246 'web-server/public/js/lib/build/build.js',
247 'web-server/public/js/lib/socket.io.js'];
248 for (var i = 0; i < unlinkFiles.length; ++i) {
249 fs.unlinkSync(path.resolve(ph, unlinkFiles[i]));
250 }
251
252 fs.renameSync(path.resolve(ph, 'game-server/app.js.wss'), path.resolve(ph, 'game-server/app.js'));
253 fs.renameSync(path.resolve(ph, 'web-server/app.js.https'), path.resolve(ph, 'web-server/app.js'));
254 fs.renameSync(path.resolve(ph, 'web-server/public/js/lib/build/build.js.wss'), path.resolve(ph, 'web-server/public/js/lib/build/build.js'));
255 break;
256 case '4':
257 // use socket.io wss
258 var unlinkFiles = ['game-server/app.js.sio',
259 'game-server/app.js',
260 'game-server/app.js.udp',
261 'game-server/app.js.wss',
262 'game-server/app.js.mqtt',
263 'web-server/app.js',
264 'web-server/public/index.html',
265 'web-server/public/js/lib/pomeloclient.js'];
266 for (var i = 0; i < unlinkFiles.length; ++i) {
267 fs.unlinkSync(path.resolve(ph, unlinkFiles[i]));
268 }
269
270 fs.renameSync(path.resolve(ph, 'game-server/app.js.sio.wss'), path.resolve(ph, 'game-server/app.js'));
271 fs.renameSync(path.resolve(ph, 'web-server/app.js.https'), path.resolve(ph, 'web-server/app.js'));
272 fs.renameSync(path.resolve(ph, 'web-server/public/index.html.sio'), path.resolve(ph, 'web-server/public/index.html'));
273 fs.renameSync(path.resolve(ph, 'web-server/public/js/lib/pomeloclient.js.wss'), path.resolve(ph, 'web-server/public/js/lib/pomeloclient.js'));
274
275 rmdir(path.resolve(ph, 'web-server/public/js/lib/build'));
276 rmdir(path.resolve(ph, 'web-server/public/js/lib/local'));
277 fs.unlinkSync(path.resolve(ph, 'web-server/public/js/lib/component.json'));
278 break;
279 case '5':
280 // use socket.io wss
281 var unlinkFiles = ['game-server/app.js.sio',
282 'game-server/app.js',
283 'game-server/app.js.wss',
284 'game-server/app.js.mqtt',
285 'game-server/app.js.sio.wss',
286 'web-server/app.js.https',
287 'web-server/public/index.html',
288 'web-server/public/js/lib/component.json',
289 'web-server/public/js/lib/pomeloclient.js.wss'];
290 for (var i = 0; i < unlinkFiles.length; ++i) {
291 fs.unlinkSync(path.resolve(ph, unlinkFiles[i]));
292 }
293
294 fs.renameSync(path.resolve(ph, 'game-server/app.js.udp'), path.resolve(ph, 'game-server/app.js'));
295 rmdir(path.resolve(ph, 'web-server/public/js/lib/build'));
296 rmdir(path.resolve(ph, 'web-server/public/js/lib/local'));
297 break;
298 case '6':
299 // use socket.io
300 var unlinkFiles = ['game-server/app.js',
301 'game-server/app.js.wss',
302 'game-server/app.js.udp',
303 'game-server/app.js.sio',
304 'game-server/app.js.sio.wss',
305 'web-server/app.js.https',
306 'web-server/public/index.html',
307 'web-server/public/js/lib/component.json',
308 'web-server/public/js/lib/pomeloclient.js.wss'];
309 for (var i = 0; i < unlinkFiles.length; ++i) {
310 fs.unlinkSync(path.resolve(ph, unlinkFiles[i]));
311 }
312
313 fs.renameSync(path.resolve(ph, 'game-server/app.js.mqtt'), path.resolve(ph, 'game-server/app.js'));
314 fs.renameSync(path.resolve(ph, 'web-server/public/index.html.sio'), path.resolve(ph, 'web-server/public/index.html'));
315
316 rmdir(path.resolve(ph, 'web-server/public/js/lib/build'));
317 rmdir(path.resolve(ph, 'web-server/public/js/lib/local'));
318 break;
319 }
320 var replaceFiles = ['game-server/app.js',
321 'game-server/package.json',
322 'web-server/package.json'];
323 for (var j = 0; j < replaceFiles.length; j++) {
324 var str = fs.readFileSync(path.resolve(ph, replaceFiles[j])).toString();
325 fs.writeFileSync(path.resolve(ph, replaceFiles[j]), str.replace('$', name));
326 }
327 var f = path.resolve(ph, 'game-server/package.json');
328 var content = fs.readFileSync(f).toString();
329 fs.writeFileSync(f, content.replace('#', version));
330 }, TIME_INIT);
331}
332
333
334/**
335 * Start application.
336 *
337 * @param {Object} opts options for `start` operation
338 */
339function start(opts) {
340 var absScript = path.resolve(opts.directory, 'app.js');
341 if (!fs.existsSync(absScript)) {
342 abort(SCRIPT_NOT_FOUND);
343 }
344
345 var logDir = path.resolve(opts.directory, 'logs');
346 if (!fs.existsSync(logDir)) {
347 fs.mkdir(logDir);
348 }
349
350 var ls;
351 var type = opts.type || constants.RESERVED.ALL;
352 var params = [absScript, 'env=' + opts.env, 'type=' + type];
353 if (!!opts.id) {
354 params.push('startId=' + opts.id);
355 }
356 if (opts.daemon) {
357 ls = spawn(process.execPath, params, {detached: true, stdio: 'ignore'});
358 ls.unref();
359 console.log(DAEMON_INFO);
360 process.exit(0);
361 } else {
362 ls = spawn(process.execPath, params);
363 ls.stdout.on('data', function (data) {
364 console.log(data.toString());
365 });
366 ls.stderr.on('data', function (data) {
367 console.log(data.toString());
368 });
369 }
370}
371
372/**
373 * List myhero processes.
374 *
375 * @param {Object} opts options for `list` operation
376 */
377function list(opts) {
378 var id = 'pomelo_list_' + Date.now();
379 connectToMaster(id, opts, function (client) {
380 client.request(co.moduleId, {signal: 'list'}, function (err, data) {
381 if (err) {
382 console.error(err);
383 }
384 var servers = [];
385 for (var key in data.msg) {
386 servers.push(data.msg[key]);
387 }
388 var comparer = function (a, b) {
389 if (a.serverType < b.serverType) {
390 return -1;
391 } else if (a.serverType > b.serverType) {
392 return 1;
393 } else if (a.serverId < b.serverId) {
394 return -1;
395 } else if (a.serverId > b.serverId) {
396 return 1;
397 } else {
398 return 0;
399 }
400 };
401 servers.sort(comparer);
402 var rows = [];
403 rows.push(['serverId', 'serverType', 'pid', 'rss(M)', 'heapTotal(M)', 'heapUsed(M)', 'uptime(m)']);
404 servers.forEach(function (server) {
405 rows.push([server.serverId, server.serverType, server.pid, server.rss, server.heapTotal, server.heapUsed, server.uptime]);
406 });
407 console.log(cliff.stringifyRows(rows, ['red', 'blue', 'green', 'cyan', 'magenta', 'white', 'yellow']));
408 process.exit(0);
409 });
410 });
411}
412
413/**
414 * Add server to application.
415 *
416 * @param {Object} opts options for `add` operation
417 */
418function add(opts) {
419 var id = 'pomelo_add_' + Date.now();
420 connectToMaster(id, opts, function (client) {
421 client.request(co.moduleId, {signal: 'add', args: opts.args}, function (err) {
422 if (err) {
423 console.error(err);
424 }
425 else {
426 console.info(ADD_SERVER_INFO);
427 }
428 process.exit(0);
429 });
430 });
431}
432
433/**
434 * Terminal application.
435 *
436 * @param {String} signal stop/kill
437 * @param {Object} opts options for `stop/kill` operation
438 */
439function terminal(signal, opts) {
440 console.info(CLOSEAPP_INFO);
441 // option force just for `kill`
442 if (opts.force) {
443 if (os.platform() === constants.PLATFORM.WIN) {
444 exec(KILL_CMD_WIN);
445 } else {
446 exec(KILL_CMD_LUX);
447 }
448 process.exit(1);
449 return;
450 }
451 var id = 'pomelo_terminal_' + Date.now();
452 connectToMaster(id, opts, function (client) {
453 client.request(co.moduleId, {
454 signal: signal, ids: opts.serverIds
455 }, function (err, msg) {
456 if (err) {
457 console.error(err);
458 }
459 if (signal === 'kill') {
460 if (msg.code === 'ok') {
461 console.log('All the servers have been terminated!');
462 } else {
463 console.log('There may be some servers remained:', msg.serverIds);
464 }
465 }
466 process.exit(0);
467 });
468 });
469}
470
471function restart(opts) {
472 var id = 'pomelo_restart_' + Date.now();
473 var serverIds = [];
474 var type = null;
475 if (!!opts.id) {
476 serverIds.push(opts.id);
477 }
478 if (!!opts.type) {
479 type = opts.type;
480 }
481 connectToMaster(id, opts, function (client) {
482 client.request(co.moduleId, {signal: 'restart', ids: serverIds, type: type}, function (err, fails) {
483 if (!!err) {
484 console.error(err);
485 } else if (!!fails.length) {
486 console.info('restart fails server ids: %j', fails);
487 } else {
488 console.info(RESTART_SERVER_INFO);
489 }
490 process.exit(0);
491 });
492 });
493}
494
495function connectToMaster(id, opts, cb) {
496 var client = new adminClient({username: opts.username, password: opts.password, md5: true});
497 client.connect(id, opts.host, opts.port, function (err) {
498 if (err) {
499 abort(CONNECT_ERROR + err.red);
500 }
501 if (typeof cb === 'function') {
502 cb(client);
503 }
504 });
505}
506
507/**
508 * Start master slaves.
509 *
510 * @param {String} option for `startMasterha` operation
511 */
512function startMasterha(opts) {
513 var configFile = path.join(opts.directory, constants.FILEPATH.MASTER_HA);
514 if (!fs.existsSync(configFile)) {
515 abort(MASTER_HA_NOT_FOUND);
516 }
517 var masterha = require(configFile).masterha;
518 for (var i = 0; i < masterha.length; i++) {
519 var server = masterha[i];
520 server.mode = constants.RESERVED.STAND_ALONE;
521 server.masterha = 'true';
522 server.home = opts.directory;
523 runServer(server);
524 }
525}
526
527/**
528 * Check if the given directory `path` is empty.
529 *
530 * @param {String} path
531 * @param {Function} fn
532 */
533function emptyDirectory(path, fn) {
534 fs.readdir(path, function (err, files) {
535 if (err && 'ENOENT' !== err.code) {
536 abort(FILEREAD_ERROR);
537 }
538 fn(!files || !files.length);
539 });
540}
541
542/**
543 * Prompt confirmation with the given `msg`.
544 *
545 * @param {String} msg
546 * @param {Function} fn
547 */
548function confirm(msg, fn) {
549 prompt(msg, function (val) {
550 fn(/^ *y(es)?/i.test(val));
551 });
552}
553
554/**
555 * Prompt input with the given `msg` and callback `fn`.
556 *
557 * @param {String} msg
558 * @param {Function} fn
559 */
560function prompt(msg, fn) {
561 if (' ' === msg[msg.length - 1]) {
562 process.stdout.write(msg);
563 } else {
564 console.log(msg);
565 }
566 process.stdin.setEncoding('ascii');
567 process.stdin.once('data', function (data) {
568 fn(data);
569 }).resume();
570}
571
572/**
573 * Exit with the given `str`.
574 *
575 * @param {String} str
576 */
577function abort(str) {
578 console.error(str);
579 process.exit(1);
580}
581
582/**
583 * Copy template files to project.
584 *
585 * @param {String} origin
586 * @param {String} target
587 */
588function copy(origin, target) {
589 if (!fs.existsSync(origin)) {
590 abort(origin + 'does not exist.');
591 }
592 if (!fs.existsSync(target)) {
593 mkdir(target);
594 console.log(' create : '.green + target);
595 }
596 fs.readdir(origin, function (err, datalist) {
597 if (err) {
598 abort(FILEREAD_ERROR);
599 }
600 for (var i = 0; i < datalist.length; i++) {
601 var oCurrent = path.resolve(origin, datalist[i]);
602 var tCurrent = path.resolve(target, datalist[i]);
603 if (fs.statSync(oCurrent).isFile()) {
604 fs.writeFileSync(tCurrent, fs.readFileSync(oCurrent, ''), '');
605 console.log(' create : '.green + tCurrent);
606 } else if (fs.statSync(oCurrent).isDirectory()) {
607 copy(oCurrent, tCurrent);
608 }
609 }
610 });
611}
612
613/**
614 * Mkdir -p.
615 *
616 * @param {String} path
617 * @param {Function} fn
618 */
619function mkdir(path, fn) {
620 mkdirp(path, 0755, function (err) {
621 if (err) {
622 throw err;
623 }
624 console.log(' create : '.green + path);
625 if (typeof fn === 'function') {
626 fn();
627 }
628 });
629}
630
631/**
632 * Get user's choice on connector selecting
633 *
634 * @param {Function} cb
635 */
636function connectorType(cb) {
637 prompt('Please select underly connector, 1 for websocket(native socket), 2 for socket.io, 3 for wss, 4 for socket.io(wss), 5 for udp, 6 for mqtt: [1]', function (msg) {
638 switch (msg.trim()) {
639 case '':
640 cb(1);
641 break;
642 case '1':
643 case '2':
644 case '3':
645 case '4':
646 case '5':
647 case '6':
648 cb(msg.trim());
649 break;
650 default:
651 console.log('Invalid choice! Please input 1 - 5.'.red + '\n');
652 connectorType(cb);
653 break;
654 }
655 });
656}
657
658/**
659 * Run server.
660 *
661 * @param {Object} server server information
662 */
663function runServer(server) {
664 var cmd, key;
665 var main = path.resolve(server.home, 'app.js');
666 if (utils.isLocal(server.host)) {
667 var options = [];
668 options.push(main);
669 for (key in server) {
670 options.push(util.format('%s=%s', key, server[key]));
671 }
672 starter.localrun(process.execPath, null, options);
673 } else {
674 cmd = util.format('cd "%s" && "%s"', server.home, process.execPath);
675 cmd += util.format(' "%s" ', main);
676 for (key in server) {
677 cmd += util.format(' %s=%s ', key, server[key]);
678 }
679 starter.sshrun(cmd, server.host);
680 }
681}
\No newline at end of file