123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681 |
- #!/usr/bin/env node
- /**
- * Module dependencies.
- */
- var fs = require('fs'),
- os = require('os'),
- path = require('path'),
- util = require('util'),
- cliff = require('cliff'),
- mkdirp = require('mkdirp'),
- co = require('../lib/modules/console'),
- utils = require('../lib/util/utils'),
- starter = require('../lib/master/starter'),
- exec = require('child_process').exec,
- spawn = require('child_process').spawn,
- version = require('../package.json').version,
- adminClient = require('pomelo-admin').adminClient,
- constants = require('../lib/util/constants'),
- program = require('commander');
- /**
- * Constant Variables
- */
- var TIME_INIT = 1 * 1000;
- var TIME_KILL_WAIT = 5 * 1000;
- var KILL_CMD_LUX = 'kill -9 `ps -ef|grep node|awk \'{print $2}\'`';
- var KILL_CMD_WIN = 'taskkill /im node.exe /f';
- var CUR_DIR = process.cwd();
- var DEFAULT_GAME_SERVER_DIR = CUR_DIR;
- var DEFAULT_USERNAME = 'admin';
- var DEFAULT_PWD = 'admin';
- var DEFAULT_ENV = 'development';
- var DEFAULT_MASTER_HOST = '127.0.0.1';
- var DEFAULT_MASTER_PORT = 3005;
- var CONNECT_ERROR = 'Fail to connect to admin console server.';
- var FILEREAD_ERROR = 'Fail to read the file, please check if the application is started legally.';
- var CLOSEAPP_INFO = 'Closing the application......\nPlease wait......';
- var ADD_SERVER_INFO = 'Successfully add server.';
- var RESTART_SERVER_INFO = 'Successfully restart server.';
- var 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 ';
- var 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;
- var 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;
- var COMMAND_ERROR = 'Illegal command format. Use `pomelo --help` to get more info.\n'.red;
- var DAEMON_INFO = 'The application is running in the background now.\n';
- program.version(version);
- program.command('init [path]')
- .description('create a new application')
- .action(function(path) {
- init(path || CUR_DIR);
- });
- program.command('start')
- .description('start the application')
- .option('-e, --env <env>', 'the used environment', DEFAULT_ENV)
- .option('-D, --daemon', 'enable the daemon start')
- .option('-d, --directory, <directory>', 'the code directory', DEFAULT_GAME_SERVER_DIR)
- .option('-t, --type <server-type>,', 'start server type')
- .option('-i, --id <server-id>', 'start server id')
- .action(function(opts) {
- start(opts);
- });
- program.command('list')
- .description('list the servers')
- .option('-u, --username <username>', 'administration user name', DEFAULT_USERNAME)
- .option('-p, --password <password>', 'administration password', DEFAULT_PWD)
- .option('-h, --host <master-host>', 'master server host', DEFAULT_MASTER_HOST)
- .option('-P, --port <master-port>', 'master server port', DEFAULT_MASTER_PORT)
- .action(function(opts) {
- list(opts);
- });
- program.command('add')
- .description('add a new server')
- .option('-u, --username <username>', 'administration user name', DEFAULT_USERNAME)
- .option('-p, --password <password>', 'administration password', DEFAULT_PWD)
- .option('-h, --host <master-host>', 'master server host', DEFAULT_MASTER_HOST)
- .option('-P, --port <master-port>', 'master server port', DEFAULT_MASTER_PORT)
- .action(function() {
- var args = [].slice.call(arguments, 0);
- var opts = args[args.length - 1];
- opts.args = args.slice(0, -1);
- add(opts);
- });
- program.command('stop')
- .description('stop the servers, for multiple servers, use `pomelo stop server-id-1 server-id-2`')
- .option('-u, --username <username>', 'administration user name', DEFAULT_USERNAME)
- .option('-p, --password <password>', 'administration password', DEFAULT_PWD)
- .option('-h, --host <master-host>', 'master server host', DEFAULT_MASTER_HOST)
- .option('-P, --port <master-port>', 'master server port', DEFAULT_MASTER_PORT)
- .action(function() {
- var args = [].slice.call(arguments, 0);
- var opts = args[args.length - 1];
- opts.serverIds = args.slice(0, -1);
- terminal('stop', opts);
- });
- program.command('kill')
- .description('kill the application')
- .option('-u, --username <username>', 'administration user name', DEFAULT_USERNAME)
- .option('-p, --password <password>', 'administration password', DEFAULT_PWD)
- .option('-h, --host <master-host>', 'master server host', DEFAULT_MASTER_HOST)
- .option('-P, --port <master-port>', 'master server port', DEFAULT_MASTER_PORT)
- .option('-f, --force', 'using this option would kill all the node processes')
- .action(function() {
- var args = [].slice.call(arguments, 0);
- var opts = args[args.length - 1];
- opts.serverIds = args.slice(0, -1);
- terminal('kill', opts);
- });
- program.command('restart')
- .description('restart the servers, for multiple servers, use `pomelo restart server-id-1 server-id-2`')
- .option('-u, --username <username>', 'administration user name', DEFAULT_USERNAME)
- .option('-p, --password <password>', 'administration password', DEFAULT_PWD)
- .option('-h, --host <master-host>', 'master server host', DEFAULT_MASTER_HOST)
- .option('-P, --port <master-port>', 'master server port', DEFAULT_MASTER_PORT)
- .option('-t, --type <server-type>,', 'start server type')
- .option('-i, --id <server-id>', 'start server id')
- .action(function(opts) {
- restart(opts);
- });
- program.command('masterha')
- .description('start all the slaves of the master')
- .option('-d, --directory <directory>', 'the code directory', DEFAULT_GAME_SERVER_DIR)
- .action(function(opts) {
- startMasterha(opts);
- });
- program.command('*')
- .action(function() {
- console.log(COMMAND_ERROR);
- });
- program.parse(process.argv);
- /**
- * Init application at the given directory `path`.
- *
- * @param {String} path
- */
- function init(path) {
- console.log(INIT_PROJ_NOTICE);
- connectorType(function(type) {
- emptyDirectory(path, function(empty) {
- if(empty) {
- process.stdin.destroy();
- createApplicationAt(path, type);
- } else {
- confirm('Destination is not empty, continue? (y/n) [no] ', function(force) {
- process.stdin.destroy();
- if(force) {
- createApplicationAt(path, type);
- } else {
- abort('Fail to init a project'.red);
- }
- });
- }
- });
- });
- }
- /**
- * Create directory and files at the given directory `path`.
- *
- * @param {String} ph
- */
- function createApplicationAt(ph, type) {
- var name = path.basename(path.resolve(CUR_DIR, ph));
- copy(path.join(__dirname, '../template/'), ph);
- mkdir(path.join(ph, 'game-server/logs'));
- mkdir(path.join(ph, 'shared'));
- // rmdir -r
- var rmdir = function(dir) {
- var list = fs.readdirSync(dir);
- for(var i = 0; i < list.length; i++) {
- var filename = path.join(dir, list[i]);
- var stat = fs.statSync(filename);
- if(filename === "." || filename === "..") {
- } else if(stat.isDirectory()) {
- rmdir(filename);
- } else {
- fs.unlinkSync(filename);
- }
- }
- fs.rmdirSync(dir);
- };
- setTimeout(function() {
- switch(type) {
- case '1':
- // use websocket
- var unlinkFiles = ['game-server/app.js.sio',
- 'game-server/app.js.wss',
- 'game-server/app.js.mqtt',
- 'game-server/app.js.sio.wss',
- 'game-server/app.js.udp',
- 'web-server/app.js.https',
- 'web-server/public/index.html.sio',
- 'web-server/public/js/lib/pomeloclient.js',
- 'web-server/public/js/lib/pomeloclient.js.wss',
- 'web-server/public/js/lib/build/build.js.wss',
- 'web-server/public/js/lib/socket.io.js'];
- for(var i = 0; i < unlinkFiles.length; ++i) {
- fs.unlinkSync(path.resolve(ph, unlinkFiles[i]));
- }
- break;
- case '2':
- // use socket.io
- var unlinkFiles = ['game-server/app.js',
- 'game-server/app.js.wss',
- 'game-server/app.js.udp',
- 'game-server/app.js.mqtt',
- 'game-server/app.js.sio.wss',
- 'web-server/app.js.https',
- 'web-server/public/index.html',
- 'web-server/public/js/lib/component.json',
- 'web-server/public/js/lib/pomeloclient.js.wss'];
- for(var i = 0; i < unlinkFiles.length; ++i) {
- fs.unlinkSync(path.resolve(ph, unlinkFiles[i]));
- }
- fs.renameSync(path.resolve(ph, 'game-server/app.js.sio'), path.resolve(ph, 'game-server/app.js'));
- fs.renameSync(path.resolve(ph, 'web-server/public/index.html.sio'), path.resolve(ph, 'web-server/public/index.html'));
- rmdir(path.resolve(ph, 'web-server/public/js/lib/build'));
- rmdir(path.resolve(ph, 'web-server/public/js/lib/local'));
- break;
- case '3':
- // use websocket wss
- var unlinkFiles = ['game-server/app.js.sio',
- 'game-server/app.js',
- 'game-server/app.js.udp',
- 'game-server/app.js.sio.wss',
- 'game-server/app.js.mqtt',
- 'web-server/app.js',
- 'web-server/public/index.html.sio',
- 'web-server/public/js/lib/pomeloclient.js',
- 'web-server/public/js/lib/pomeloclient.js.wss',
- 'web-server/public/js/lib/build/build.js',
- 'web-server/public/js/lib/socket.io.js'];
- for(var i = 0; i < unlinkFiles.length; ++i) {
- fs.unlinkSync(path.resolve(ph, unlinkFiles[i]));
- }
- fs.renameSync(path.resolve(ph, 'game-server/app.js.wss'), path.resolve(ph, 'game-server/app.js'));
- fs.renameSync(path.resolve(ph, 'web-server/app.js.https'), path.resolve(ph, 'web-server/app.js'));
- 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'));
- break;
- case '4':
- // use socket.io wss
- var unlinkFiles = ['game-server/app.js.sio',
- 'game-server/app.js',
- 'game-server/app.js.udp',
- 'game-server/app.js.wss',
- 'game-server/app.js.mqtt',
- 'web-server/app.js',
- 'web-server/public/index.html',
- 'web-server/public/js/lib/pomeloclient.js'];
- for(var i = 0; i < unlinkFiles.length; ++i) {
- fs.unlinkSync(path.resolve(ph, unlinkFiles[i]));
- }
- fs.renameSync(path.resolve(ph, 'game-server/app.js.sio.wss'), path.resolve(ph, 'game-server/app.js'));
- fs.renameSync(path.resolve(ph, 'web-server/app.js.https'), path.resolve(ph, 'web-server/app.js'));
- fs.renameSync(path.resolve(ph, 'web-server/public/index.html.sio'), path.resolve(ph, 'web-server/public/index.html'));
- fs.renameSync(path.resolve(ph, 'web-server/public/js/lib/pomeloclient.js.wss'), path.resolve(ph, 'web-server/public/js/lib/pomeloclient.js'));
- rmdir(path.resolve(ph, 'web-server/public/js/lib/build'));
- rmdir(path.resolve(ph, 'web-server/public/js/lib/local'));
- fs.unlinkSync(path.resolve(ph, 'web-server/public/js/lib/component.json'));
- break;
- case '5':
- // use socket.io wss
- var unlinkFiles = ['game-server/app.js.sio',
- 'game-server/app.js',
- 'game-server/app.js.wss',
- 'game-server/app.js.mqtt',
- 'game-server/app.js.sio.wss',
- 'web-server/app.js.https',
- 'web-server/public/index.html',
- 'web-server/public/js/lib/component.json',
- 'web-server/public/js/lib/pomeloclient.js.wss'];
- for(var i = 0; i < unlinkFiles.length; ++i) {
- fs.unlinkSync(path.resolve(ph, unlinkFiles[i]));
- }
-
- fs.renameSync(path.resolve(ph, 'game-server/app.js.udp'), path.resolve(ph, 'game-server/app.js'));
- rmdir(path.resolve(ph, 'web-server/public/js/lib/build'));
- rmdir(path.resolve(ph, 'web-server/public/js/lib/local'));
- break;
- case '6':
- // use socket.io
- var unlinkFiles = ['game-server/app.js',
- 'game-server/app.js.wss',
- 'game-server/app.js.udp',
- 'game-server/app.js.sio',
- 'game-server/app.js.sio.wss',
- 'web-server/app.js.https',
- 'web-server/public/index.html',
- 'web-server/public/js/lib/component.json',
- 'web-server/public/js/lib/pomeloclient.js.wss'];
- for(var i = 0; i < unlinkFiles.length; ++i) {
- fs.unlinkSync(path.resolve(ph, unlinkFiles[i]));
- }
- fs.renameSync(path.resolve(ph, 'game-server/app.js.mqtt'), path.resolve(ph, 'game-server/app.js'));
- fs.renameSync(path.resolve(ph, 'web-server/public/index.html.sio'), path.resolve(ph, 'web-server/public/index.html'));
- rmdir(path.resolve(ph, 'web-server/public/js/lib/build'));
- rmdir(path.resolve(ph, 'web-server/public/js/lib/local'));
- break;
- }
- var replaceFiles = ['game-server/app.js',
- 'game-server/package.json',
- 'web-server/package.json'];
- for(var j = 0; j < replaceFiles.length; j++) {
- var str = fs.readFileSync(path.resolve(ph, replaceFiles[j])).toString();
- fs.writeFileSync(path.resolve(ph, replaceFiles[j]), str.replace('$', name));
- }
- var f = path.resolve(ph, 'game-server/package.json');
- var content = fs.readFileSync(f).toString();
- fs.writeFileSync(f, content.replace('#', version));
- }, TIME_INIT);
- }
- /**
- * Start application.
- *
- * @param {Object} opts options for `start` operation
- */
- function start(opts) {
- var absScript = path.resolve(opts.directory, 'app.js');
- if (!fs.existsSync(absScript)) {
- abort(SCRIPT_NOT_FOUND);
- }
- var logDir = path.resolve(opts.directory, 'logs');
- if (!fs.existsSync(logDir)) {
- fs.mkdir(logDir);
- }
-
- var ls;
- var type = opts.type || constants.RESERVED.ALL;
- var params = [absScript, 'env=' + opts.env, 'type=' + type];
- if(!!opts.id) {
- params.push('startId=' + opts.id);
- }
- if (opts.daemon) {
- ls = spawn(process.execPath, params, {detached: true, stdio: 'ignore'});
- ls.unref();
- console.log(DAEMON_INFO);
- process.exit(0);
- } else {
- ls = spawn(process.execPath, params);
- ls.stdout.on('data', function(data) {
- console.log(data.toString());
- });
- ls.stderr.on('data', function(data) {
- console.log(data.toString());
- });
- }
- }
- /**
- * List pomelo processes.
- *
- * @param {Object} opts options for `list` operation
- */
- function list(opts) {
- var id = 'pomelo_list_' + Date.now();
- connectToMaster(id, opts, function(client) {
- client.request(co.moduleId, {signal: 'list'}, function(err, data) {
- if(err) {
- console.error(err);
- }
- var servers = [];
- for(var key in data.msg) {
- servers.push(data.msg[key]);
- }
- var comparer = function(a, b) {
- if (a.serverType < b.serverType) {
- return -1;
- } else if (a.serverType > b.serverType) {
- return 1;
- } else if (a.serverId < b.serverId) {
- return -1;
- } else if (a.serverId > b.serverId) {
- return 1;
- } else {
- return 0;
- }
- };
- servers.sort(comparer);
- var rows = [];
- rows.push(['serverId', 'serverType', 'pid', 'rss(M)', 'heapTotal(M)', 'heapUsed(M)', 'uptime(m)']);
- servers.forEach(function(server) {
- rows.push([server.serverId, server.serverType, server.pid, server.rss, server.heapTotal, server.heapUsed, server.uptime]);
- });
- console.log(cliff.stringifyRows(rows, ['red', 'blue', 'green', 'cyan', 'magenta', 'white', 'yellow']));
- process.exit(0);
- });
- });
- }
- /**
- * Add server to application.
- *
- * @param {Object} opts options for `add` operation
- */
- function add(opts) {
- var id = 'pomelo_add_' + Date.now();
- connectToMaster(id, opts, function(client) {
- client.request(co.moduleId, { signal: 'add', args: opts.args }, function(err) {
- if(err) {
- console.error(err);
- }
- else {
- console.info(ADD_SERVER_INFO);
- }
- process.exit(0);
- });
- });
- }
- /**
- * Terminal application.
- *
- * @param {String} signal stop/kill
- * @param {Object} opts options for `stop/kill` operation
- */
- function terminal(signal, opts) {
- console.info(CLOSEAPP_INFO);
- // option force just for `kill`
- if(opts.force) {
- if (os.platform() === constants.PLATFORM.WIN) {
- exec(KILL_CMD_WIN);
- } else {
- exec(KILL_CMD_LUX);
- }
- process.exit(1);
- return;
- }
- var id = 'pomelo_terminal_' + Date.now();
- connectToMaster(id, opts, function(client) {
- client.request(co.moduleId, {
- signal: signal, ids: opts.serverIds
- }, function(err, msg) {
- if(err) {
- console.error(err);
- }
- if(signal === 'kill') {
- if(msg.code === 'ok') {
- console.log('All the servers have been terminated!');
- } else {
- console.log('There may be some servers remained:', msg.serverIds);
- }
- }
- process.exit(0);
- });
- });
- }
- function restart(opts) {
- var id = 'pomelo_restart_' + Date.now();
- var serverIds = [];
- var type = null;
- if(!!opts.id) {
- serverIds.push(opts.id);
- }
- if(!!opts.type) {
- type = opts.type;
- }
- connectToMaster(id, opts, function(client) {
- client.request(co.moduleId, { signal: 'restart', ids: serverIds, type: type}, function(err, fails) {
- if(!!err) {
- console.error(err);
- } else if(!!fails.length) {
- console.info('restart fails server ids: %j', fails);
- } else {
- console.info(RESTART_SERVER_INFO);
- }
- process.exit(0);
- });
- });
- }
- function connectToMaster(id, opts, cb) {
- var client = new adminClient({username: opts.username, password: opts.password, md5: true});
- client.connect(id, opts.host, opts.port, function(err) {
- if(err) {
- abort(CONNECT_ERROR + err.red);
- }
- if(typeof cb === 'function') {
- cb(client);
- }
- });
- }
- /**
- * Start master slaves.
- *
- * @param {String} option for `startMasterha` operation
- */
- function startMasterha(opts) {
- var configFile = path.join(opts.directory, constants.FILEPATH.MASTER_HA);
- if(!fs.existsSync(configFile)) {
- abort(MASTER_HA_NOT_FOUND);
- }
- var masterha = require(configFile).masterha;
- for(var i=0; i<masterha.length; i++) {
- var server = masterha[i];
- server.mode = constants.RESERVED.STAND_ALONE;
- server.masterha = 'true';
- server.home = opts.directory;
- runServer(server);
- }
- }
- /**
- * Check if the given directory `path` is empty.
- *
- * @param {String} path
- * @param {Function} fn
- */
- function emptyDirectory(path, fn) {
- fs.readdir(path, function(err, files) {
- if(err && 'ENOENT' !== err.code) {
- abort(FILEREAD_ERROR);
- }
- fn(!files || !files.length);
- });
- }
- /**
- * Prompt confirmation with the given `msg`.
- *
- * @param {String} msg
- * @param {Function} fn
- */
- function confirm(msg, fn) {
- prompt(msg, function(val) {
- fn(/^ *y(es)?/i.test(val));
- });
- }
- /**
- * Prompt input with the given `msg` and callback `fn`.
- *
- * @param {String} msg
- * @param {Function} fn
- */
- function prompt(msg, fn) {
- if(' ' === msg[msg.length - 1]) {
- process.stdout.write(msg);
- } else {
- console.log(msg);
- }
- process.stdin.setEncoding('ascii');
- process.stdin.once('data', function(data) {
- fn(data);
- }).resume();
- }
- /**
- * Exit with the given `str`.
- *
- * @param {String} str
- */
- function abort(str) {
- console.error(str);
- process.exit(1);
- }
- /**
- * Copy template files to project.
- *
- * @param {String} origin
- * @param {String} target
- */
- function copy(origin, target) {
- if(!fs.existsSync(origin)) {
- abort(origin + 'does not exist.');
- }
- if(!fs.existsSync(target)) {
- mkdir(target);
- console.log(' create : '.green + target);
- }
- fs.readdir(origin, function(err, datalist) {
- if(err) {
- abort(FILEREAD_ERROR);
- }
- for(var i = 0; i < datalist.length; i++) {
- var oCurrent = path.resolve(origin, datalist[i]);
- var tCurrent = path.resolve(target, datalist[i]);
- if(fs.statSync(oCurrent).isFile()) {
- fs.writeFileSync(tCurrent, fs.readFileSync(oCurrent, ''), '');
- console.log(' create : '.green + tCurrent);
- } else if(fs.statSync(oCurrent).isDirectory()) {
- copy(oCurrent, tCurrent);
- }
- }
- });
- }
- /**
- * Mkdir -p.
- *
- * @param {String} path
- * @param {Function} fn
- */
- function mkdir(path, fn) {
- mkdirp(path, 0755, function(err){
- if(err) {
- throw err;
- }
- console.log(' create : '.green + path);
- if(typeof fn === 'function') {
- fn();
- }
- });
- }
- /**
- * Get user's choice on connector selecting
- *
- * @param {Function} cb
- */
- function connectorType(cb) {
- 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) {
- switch(msg.trim()) {
- case '':
- cb(1);
- break;
- case '1':
- case '2':
- case '3':
- case '4':
- case '5':
- case '6':
- cb(msg.trim());
- break;
- default:
- console.log('Invalid choice! Please input 1 - 5.'.red + '\n');
- connectorType(cb);
- break;
- }
- });
- }
- /**
- * Run server.
- *
- * @param {Object} server server information
- */
- function runServer(server) {
- var cmd, key;
- var main = path.resolve(server.home, 'app.js');
- if(utils.isLocal(server.host)) {
- var options = [];
- options.push(main);
- for(key in server) {
- options.push(util.format('%s=%s', key, server[key]));
- }
- starter.localrun(process.execPath, null, options);
- } else {
- cmd = util.format('cd "%s" && "%s"', server.home, process.execPath);
- cmd += util.format(' "%s" ', main);
- for(key in server) {
- cmd += util.format(' %s=%s ', key, server[key]);
- }
- starter.sshrun(cmd, server.host);
- }
- }
|