UNPKG

24.5 kBPlain TextView Raw
1#!/usr/bin/env node
2
3/**
4 * Module dependencies.
5 */
6let 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 */
25let TIME_INIT = 1 * 1000;
26let TIME_KILL_WAIT = 5 * 1000;
27let KILL_CMD_LUX = 'kill -9 `ps -ef|grep node|awk \'{print $2}\'`';
28let KILL_CMD_WIN = 'taskkill /im node.exe /f';
29
30let CUR_DIR = process.cwd();
31let DEFAULT_GAME_SERVER_DIR = CUR_DIR;
32let DEFAULT_USERNAME = 'admin';
33let DEFAULT_PWD = 'admin';
34let DEFAULT_ENV = 'development';
35let DEFAULT_MASTER_HOST = '127.0.0.1';
36let DEFAULT_MASTER_PORT = 3005;
37
38let CONNECT_ERROR = 'Fail to connect to admin console server.';
39let FILEREAD_ERROR = 'Fail to read the file, please check if the application is started legally.';
40let CLOSEAPP_INFO = 'Closing the application......\nPlease wait......';
41let ADD_SERVER_INFO = 'Successfully add server.';
42let RESTART_SERVER_INFO = 'Successfully restart server.';
43let 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 ';
44let 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;
45let 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;
46let COMMAND_ERROR = 'Illegal command format. Use `myhero --help` to get more info.\n'.red;
47let 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 let args = [].slice.call(arguments, 0);
86 let 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 let args = [].slice.call(arguments, 0);
99 let 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 let args = [].slice.call(arguments, 0);
113 let 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 let 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 let rmdir = function (dir) {
182 let list = fs.readdirSync(dir);
183 for (let i = 0; i < list.length; i++) {
184 let filename = path.join(dir, list[i]);
185 let 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 let unlinkFiles;
197 switch (type) {
198 case '1':
199 // use websocket
200 unlinkFiles = ['game-server/app.js.sio',
201 'game-server/app.js.wss',
202 'game-server/app.js.mqtt',
203 'game-server/app.js.sio.wss',
204 'game-server/app.js.udp',
205 'web-server/app.js.https',
206 'web-server/public/index.html.sio',
207 'web-server/public/js/lib/myheroclient.js',
208 'web-server/public/js/lib/myheroclient.js.wss',
209 'web-server/public/js/lib/build/build.js.wss',
210 'web-server/public/js/lib/socket.io.js'];
211 for (let i = 0; i < unlinkFiles.length; ++i) {
212 fs.unlinkSync(path.resolve(ph, unlinkFiles[i]));
213 }
214 break;
215 case '2':
216 // use socket.io
217 unlinkFiles = ['game-server/app.js',
218 'game-server/app.js.wss',
219 'game-server/app.js.udp',
220 'game-server/app.js.mqtt',
221 'game-server/app.js.sio.wss',
222 'web-server/app.js.https',
223 'web-server/public/index.html',
224 'web-server/public/js/lib/component.json',
225 'web-server/public/js/lib/myheroclient.js.wss'];
226 for (let i = 0; i < unlinkFiles.length; ++i) {
227 fs.unlinkSync(path.resolve(ph, unlinkFiles[i]));
228 }
229
230 fs.renameSync(path.resolve(ph, 'game-server/app.js.sio'), path.resolve(ph, 'game-server/app.js'));
231 fs.renameSync(path.resolve(ph, 'web-server/public/index.html.sio'), path.resolve(ph, 'web-server/public/index.html'));
232
233 rmdir(path.resolve(ph, 'web-server/public/js/lib/build'));
234 rmdir(path.resolve(ph, 'web-server/public/js/lib/local'));
235 break;
236 case '3':
237 // use websocket wss
238 unlinkFiles = ['game-server/app.js.sio',
239 'game-server/app.js',
240 'game-server/app.js.udp',
241 'game-server/app.js.sio.wss',
242 'game-server/app.js.mqtt',
243 'web-server/app.js',
244 'web-server/public/index.html.sio',
245 'web-server/public/js/lib/myheroclient.js',
246 'web-server/public/js/lib/myheroclient.js.wss',
247 'web-server/public/js/lib/build/build.js',
248 'web-server/public/js/lib/socket.io.js'];
249 for (let i = 0; i < unlinkFiles.length; ++i) {
250 fs.unlinkSync(path.resolve(ph, unlinkFiles[i]));
251 }
252
253 fs.renameSync(path.resolve(ph, 'game-server/app.js.wss'), path.resolve(ph, 'game-server/app.js'));
254 fs.renameSync(path.resolve(ph, 'web-server/app.js.https'), path.resolve(ph, 'web-server/app.js'));
255 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'));
256 break;
257 case '4':
258 // use socket.io wss
259 unlinkFiles = ['game-server/app.js.sio',
260 'game-server/app.js',
261 'game-server/app.js.udp',
262 'game-server/app.js.wss',
263 'game-server/app.js.mqtt',
264 'web-server/app.js',
265 'web-server/public/index.html',
266 'web-server/public/js/lib/myheroclient.js'];
267 for (let i = 0; i < unlinkFiles.length; ++i) {
268 fs.unlinkSync(path.resolve(ph, unlinkFiles[i]));
269 }
270
271 fs.renameSync(path.resolve(ph, 'game-server/app.js.sio.wss'), path.resolve(ph, 'game-server/app.js'));
272 fs.renameSync(path.resolve(ph, 'web-server/app.js.https'), path.resolve(ph, 'web-server/app.js'));
273 fs.renameSync(path.resolve(ph, 'web-server/public/index.html.sio'), path.resolve(ph, 'web-server/public/index.html'));
274 fs.renameSync(path.resolve(ph, 'web-server/public/js/lib/myheroclient.js.wss'), path.resolve(ph, 'web-server/public/js/lib/myheroclient.js'));
275
276 rmdir(path.resolve(ph, 'web-server/public/js/lib/build'));
277 rmdir(path.resolve(ph, 'web-server/public/js/lib/local'));
278 fs.unlinkSync(path.resolve(ph, 'web-server/public/js/lib/component.json'));
279 break;
280 case '5':
281 // use socket.io wss
282 unlinkFiles = ['game-server/app.js.sio',
283 'game-server/app.js',
284 'game-server/app.js.wss',
285 'game-server/app.js.mqtt',
286 'game-server/app.js.sio.wss',
287 'web-server/app.js.https',
288 'web-server/public/index.html',
289 'web-server/public/js/lib/component.json',
290 'web-server/public/js/lib/myheroclient.js.wss'];
291 for (let i = 0; i < unlinkFiles.length; ++i) {
292 fs.unlinkSync(path.resolve(ph, unlinkFiles[i]));
293 }
294
295 fs.renameSync(path.resolve(ph, 'game-server/app.js.udp'), path.resolve(ph, 'game-server/app.js'));
296 rmdir(path.resolve(ph, 'web-server/public/js/lib/build'));
297 rmdir(path.resolve(ph, 'web-server/public/js/lib/local'));
298 break;
299 case '6':
300 // use socket.io
301 unlinkFiles = ['game-server/app.js',
302 'game-server/app.js.wss',
303 'game-server/app.js.udp',
304 'game-server/app.js.sio',
305 'game-server/app.js.sio.wss',
306 'web-server/app.js.https',
307 'web-server/public/index.html',
308 'web-server/public/js/lib/component.json',
309 'web-server/public/js/lib/myheroclient.js.wss'];
310 for (let i = 0; i < unlinkFiles.length; ++i) {
311 fs.unlinkSync(path.resolve(ph, unlinkFiles[i]));
312 }
313
314 fs.renameSync(path.resolve(ph, 'game-server/app.js.mqtt'), path.resolve(ph, 'game-server/app.js'));
315 fs.renameSync(path.resolve(ph, 'web-server/public/index.html.sio'), path.resolve(ph, 'web-server/public/index.html'));
316
317 rmdir(path.resolve(ph, 'web-server/public/js/lib/build'));
318 rmdir(path.resolve(ph, 'web-server/public/js/lib/local'));
319 break;
320 }
321 let replaceFiles = ['game-server/app.js',
322 'game-server/package.json',
323 'web-server/package.json'];
324 for (let j = 0; j < replaceFiles.length; j++) {
325 let str = fs.readFileSync(path.resolve(ph, replaceFiles[j])).toString();
326 fs.writeFileSync(path.resolve(ph, replaceFiles[j]), str.replace('$', name));
327 }
328 let f = path.resolve(ph, 'game-server/package.json');
329 let content = fs.readFileSync(f).toString();
330 fs.writeFileSync(f, content.replace('#', version));
331 }, TIME_INIT);
332}
333
334
335/**
336 * Start application.
337 *
338 * @param {Object} opts options for `start` operation
339 */
340function start(opts) {
341 let absScript = path.resolve(opts.directory, 'app.js');
342 if (!fs.existsSync(absScript)) {
343 abort(SCRIPT_NOT_FOUND);
344 }
345
346 let logDir = path.resolve(opts.directory, 'logs');
347 if (!fs.existsSync(logDir)) {
348 fs.mkdir(logDir);
349 }
350
351 let ls;
352 let type = opts.type || constants.RESERVED.ALL;
353 let params = [absScript, 'env=' + opts.env, 'type=' + type];
354 if (!!opts.id) {
355 params.push('startId=' + opts.id);
356 }
357 if (opts.daemon) {
358 ls = spawn(process.execPath, params, {detached: true, stdio: 'ignore'});
359 ls.unref();
360 console.log(DAEMON_INFO);
361 process.exit(0);
362 } else {
363 ls = spawn(process.execPath, params);
364 ls.stdout.on('data', function (data) {
365 console.log(data.toString());
366 });
367 ls.stderr.on('data', function (data) {
368 console.log(data.toString());
369 });
370 }
371}
372
373/**
374 * List myhero processes.
375 *
376 * @param {Object} opts options for `list` operation
377 */
378function list(opts) {
379 let id = 'myhero_list_' + Date.now();
380 connectToMaster(id, opts, function (client) {
381 client.request(co.moduleId, {signal: 'list'}, function (err, data) {
382 if (err) {
383 console.error(err);
384 }
385 let servers = [];
386 for (let key in data.msg) {
387 servers.push(data.msg[key]);
388 }
389 let comparer = function (a, b) {
390 if (a.serverType < b.serverType) {
391 return -1;
392 } else if (a.serverType > b.serverType) {
393 return 1;
394 } else if (a.serverId < b.serverId) {
395 return -1;
396 } else if (a.serverId > b.serverId) {
397 return 1;
398 } else {
399 return 0;
400 }
401 };
402 servers.sort(comparer);
403 let rows = [];
404 rows.push(['serverId', 'serverType', 'pid', 'rss(M)', 'heapTotal(M)', 'heapUsed(M)', 'uptime(m)']);
405 servers.forEach(function (server) {
406 rows.push([server.serverId, server.serverType, server.pid, server.rss, server.heapTotal, server.heapUsed, server.uptime]);
407 });
408 console.log(cliff.stringifyRows(rows, ['red', 'blue', 'green', 'cyan', 'magenta', 'white', 'yellow']));
409 process.exit(0);
410 });
411 });
412}
413
414/**
415 * Add server to application.
416 *
417 * @param {Object} opts options for `add` operation
418 */
419function add(opts) {
420 let id = 'myhero_add_' + Date.now();
421 connectToMaster(id, opts, function (client) {
422 client.request(co.moduleId, {signal: 'add', args: opts.args}, function (err) {
423 if (err) {
424 console.error(err);
425 }
426 else {
427 console.info(ADD_SERVER_INFO);
428 }
429 process.exit(0);
430 });
431 });
432}
433
434/**
435 * Terminal application.
436 *
437 * @param {String} signal stop/kill
438 * @param {Object} opts options for `stop/kill` operation
439 */
440function terminal(signal, opts) {
441 console.info(CLOSEAPP_INFO);
442 // option force just for `kill`
443 if (opts.force) {
444 if (os.platform() === constants.PLATFORM.WIN) {
445 exec(KILL_CMD_WIN);
446 } else {
447 exec(KILL_CMD_LUX);
448 }
449 process.exit(1);
450 return;
451 }
452 let id = 'myhero_terminal_' + Date.now();
453 connectToMaster(id, opts, function (client) {
454 client.request(co.moduleId, {
455 signal: signal, ids: opts.serverIds
456 }, function (err, msg) {
457 if (err) {
458 console.error(err);
459 }
460 if (signal === 'kill') {
461 if (msg.code === 'ok') {
462 console.log('All the servers have been terminated!');
463 } else {
464 console.log('There may be some servers remained:', msg.serverIds);
465 }
466 }
467 process.exit(0);
468 });
469 });
470}
471
472function restart(opts) {
473 let id = 'myhero_restart_' + Date.now();
474 let serverIds = [];
475 let type = null;
476 if (!!opts.id) {
477 serverIds.push(opts.id);
478 }
479 if (!!opts.type) {
480 type = opts.type;
481 }
482 connectToMaster(id, opts, function (client) {
483 client.request(co.moduleId, {signal: 'restart', ids: serverIds, type: type}, function (err, fails) {
484 if (!!err) {
485 console.error(err);
486 } else if (!!fails.length) {
487 console.info('restart fails server ids: %j', fails);
488 } else {
489 console.info(RESTART_SERVER_INFO);
490 }
491 process.exit(0);
492 });
493 });
494}
495
496function connectToMaster(id, opts, cb) {
497 let client = new adminClient({username: opts.username, password: opts.password, md5: true});
498 client.connect(id, opts.host, opts.port, function (err) {
499 if (err) {
500 abort(CONNECT_ERROR + err.red);
501 }
502 if (typeof cb === 'function') {
503 cb(client);
504 }
505 });
506}
507
508/**
509 * Start master slaves.
510 *
511 * @param {String} option for `startMasterha` operation
512 */
513function startMasterha(opts) {
514 let configFile = path.join(opts.directory, constants.FILEPATH.MASTER_HA);
515 if (!fs.existsSync(configFile)) {
516 abort(MASTER_HA_NOT_FOUND);
517 }
518 let masterha = require(configFile).masterha;
519 for (let i = 0; i < masterha.length; i++) {
520 let server = masterha[i];
521 server.mode = constants.RESERVED.STAND_ALONE;
522 server.masterha = 'true';
523 server.home = opts.directory;
524 runServer(server);
525 }
526}
527
528/**
529 * Check if the given directory `path` is empty.
530 *
531 * @param {String} path
532 * @param {Function} fn
533 */
534function emptyDirectory(path, fn) {
535 fs.readdir(path, function (err, files) {
536 if (err && 'ENOENT' !== err.code) {
537 abort(FILEREAD_ERROR);
538 }
539 fn(!files || !files.length);
540 });
541}
542
543/**
544 * Prompt confirmation with the given `msg`.
545 *
546 * @param {String} msg
547 * @param {Function} fn
548 */
549function confirm(msg, fn) {
550 prompt(msg, function (val) {
551 fn(/^ *y(es)?/i.test(val));
552 });
553}
554
555/**
556 * Prompt input with the given `msg` and callback `fn`.
557 *
558 * @param {String} msg
559 * @param {Function} fn
560 */
561function prompt(msg, fn) {
562 if (' ' === msg[msg.length - 1]) {
563 process.stdout.write(msg);
564 } else {
565 console.log(msg);
566 }
567 process.stdin.setEncoding('ascii');
568 process.stdin.once('data', function (data) {
569 fn(data);
570 }).resume();
571}
572
573/**
574 * Exit with the given `str`.
575 *
576 * @param {String} str
577 */
578function abort(str) {
579 console.error(str);
580 process.exit(1);
581}
582
583/**
584 * Copy template files to project.
585 *
586 * @param {String} origin
587 * @param {String} target
588 */
589function copy(origin, target) {
590 if (!fs.existsSync(origin)) {
591 abort(origin + 'does not exist.');
592 }
593 if (!fs.existsSync(target)) {
594 mkdir(target);
595 console.log(' create : '.green + target);
596 }
597 fs.readdir(origin, function (err, datalist) {
598 if (err) {
599 abort(FILEREAD_ERROR);
600 }
601 for (let i = 0; i < datalist.length; i++) {
602 let oCurrent = path.resolve(origin, datalist[i]);
603 let tCurrent = path.resolve(target, datalist[i]);
604 if (fs.statSync(oCurrent).isFile()) {
605 fs.writeFileSync(tCurrent, fs.readFileSync(oCurrent, ''), '');
606 console.log(' create : '.green + tCurrent);
607 } else if (fs.statSync(oCurrent).isDirectory()) {
608 copy(oCurrent, tCurrent);
609 }
610 }
611 });
612}
613
614/**
615 * Mkdir -p.
616 *
617 * @param {String} path
618 * @param {Function} fn
619 */
620function mkdir(path, fn) {
621 mkdirp(path, 0o0755, function (err) {
622 if (err) {
623 throw err;
624 }
625 console.log(' create : '.green + path);
626 if (typeof fn === 'function') {
627 fn();
628 }
629 });
630}
631
632/**
633 * Get user's choice on connector selecting
634 *
635 * @param {Function} cb
636 */
637function connectorType(cb) {
638 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) {
639 switch (msg.trim()) {
640 case '':
641 cb(1);
642 break;
643 case '1':
644 case '2':
645 case '3':
646 case '4':
647 case '5':
648 case '6':
649 cb(msg.trim());
650 break;
651 default:
652 console.log('Invalid choice! Please input 1 - 5.'.red + '\n');
653 connectorType(cb);
654 break;
655 }
656 });
657}
658
659/**
660 * Run server.
661 *
662 * @param {Object} server server information
663 */
664function runServer(server) {
665 let cmd, key;
666 let main = path.resolve(server.home, 'app.js');
667 if (utils.isLocal(server.host)) {
668 let options = [];
669 options.push(main);
670 for (key in server) {
671 options.push(util.format('%s=%s', key, server[key]));
672 }
673 starter.localrun(process.execPath, null, options);
674 } else {
675 cmd = util.format('cd "%s" && "%s"', server.home, process.execPath);
676 cmd += util.format(' "%s" ', main);
677 for (key in server) {
678 cmd += util.format(' %s=%s ', key, server[key]);
679 }
680 starter.sshrun(cmd, server.host);
681 }
682}
\No newline at end of file