From d01d95470abdfc5db7b0a4e490fb18e28d1ff3e5 Mon Sep 17 00:00:00 2001 From: XGHeaven Date: Fri, 22 Dec 2017 12:28:23 +0800 Subject: [PATCH 1/3] start/stop/status finished --- .eslintrc.json | 3 +++ README.md | 64 +++++++++++++++++++++++++++++++++++++++++++++++++- bin/akyuu-bin | 3 +++ lib/cluster.js | 49 ++++++++++++++++++++++++++++++++++++++ lib/config.js | 60 ++++++++++++++++++++++++++++++++++++++++++++++ lib/index.js | 35 +++++++++++++++++++++++++++ lib/restart.js | 8 +++++++ lib/start.js | 61 +++++++++++++++++++++++++++++++++++++++++++++++ lib/stop.js | 47 ++++++++++++++++++++++++++++++++++++ lib/stsuts.js | 24 +++++++++++++++++++ package.json | 38 ++++++++++++++++++++++++++++++ 11 files changed, 391 insertions(+), 1 deletion(-) create mode 100644 .eslintrc.json create mode 100755 bin/akyuu-bin create mode 100644 lib/cluster.js create mode 100644 lib/config.js create mode 100644 lib/index.js create mode 100644 lib/restart.js create mode 100644 lib/start.js create mode 100644 lib/stop.js create mode 100644 lib/stsuts.js create mode 100644 package.json diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..17105ba --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "xadillax-style" +} \ No newline at end of file diff --git a/README.md b/README.md index bb2c49f..7d15808 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,64 @@ -# akyuu-bin +# Akyuu-bin + CLI tool for Akyuu.js. + +# Install + +``` +# npm install akyuu-bin +# or +# yarn add akyuu-bin +``` + +# Feature + +* [ ] Init project +* [x] One step to start/stop Application (akyuu-bin start/stop) +* [x] Check out application status (akyuu-bin status) +* And More... + +# Command + +## Init + +> Work in Progress + +## Start/Stop + +1. Add `cluster` config + +``` +{ + cluster: { + // ... extends akyuu-cluster config + + // mode: 'cluster' | 'fork', default: 'cluster' + mode: 'cluster', + // pid file, default: project-root/run/process.pid + pid: 'pid file', + // master log + log: { + info: 'info log path', + error: 'error log path' + } + } +} +``` + +2. Add start/stop scripts + +``` +// package.json +{ + ... + scripts: { + "start": "akyuu-pm start", + "stop": "akyuu-pm stop" + } + ... +} +``` + +3. Type your script to start/stop + +# Thanks diff --git a/bin/akyuu-bin b/bin/akyuu-bin new file mode 100755 index 0000000..a9c6ee2 --- /dev/null +++ b/bin/akyuu-bin @@ -0,0 +1,3 @@ +#!/usr/bin/env node + +require("../lib"); diff --git a/lib/cluster.js b/lib/cluster.js new file mode 100644 index 0000000..74f4002 --- /dev/null +++ b/lib/cluster.js @@ -0,0 +1,49 @@ +"use strict"; + +const fs = require("fs"); +const { Console } = require("console"); +const akyuu = require("akyuu"); + +const config = akyuu.config.cluster; +let { stdout, stderr } = process; + +if(config.log.info) { + stdout = fs.createWriteStream(config.log.info, { flags: "a" }); +} + +if(config.log.error) { + stderr = fs.createWriteStream(config.log.error, { flags: "a" }); +} + +// eslint-disable-next-line +const console = new Console(stdout, stderr); + +console.info(`${new Date().toString()} [${process.pid}] Master Started`); + +akyuu.startCluster({ + onFork(worker) { + console.info(`${new Date().toString()} [${worker.process.pid}] Fork Success: ${worker.id}`); + }, + onDisconnect(worker) { + console.info(`${new Date().toString()} [${worker.process.pid}] Disconnect: ${worker.id}`); + }, + onExit(worker, code, signal) { + console.info( + `${new Date().toString()} [${worker.process.pid}] Exit: ${worker.id} with code ${code} and ${signal}` + ); + }, + onUnexpectedExit(worker, code, signal) { + console.error( + `${new Date().toString()} [${worker.process.pid}]` + + ` Unexpected Exit: ${worker.id} with code ${code} and ${signal}` + ); + }, + onReachReforkLimit() { + console.warn(`${new Date().toString()} [${process.pid}] Reach Refork Limit`); + } +}); + +process.on("uncaughtException", err => { + console.error(`${new Date().toString()} [${process.pid}] Master Uncaught Exception`); + console.error(err); +}); diff --git a/lib/config.js b/lib/config.js new file mode 100644 index 0000000..ed75253 --- /dev/null +++ b/lib/config.js @@ -0,0 +1,60 @@ +"use strict"; + +const fs = require("fs-extra"); +const path = require("path"); +const defaultsDeep = require("lodash.defaultsdeep"); + +const defaultConfig = { + mode: "cluster", + log: { + info: "", + error: "" + }, + pid: path.join(".", "run", "master.pid"), + entry: "" +}; + +// Process Manager Config +// use config/default/cluster.js config +class PMConfig { + /** + * @param {*} root Project root + */ + constructor(root) { + // Because Akyuu.js don't have independent package to handle config + // So I have to assume all project sturcture is + // root + // -- config + // -- -- default + // -- -- -- cluster.js + // -- controllers + // -- models + // Hard coded to get cluster config + const configPath = path.join(root, "config", "default", "cluster.js"); + this.root = root; + + if(fs.pathExistsSync(configPath)) { + const config = require(configPath); + defaultsDeep(this, config, defaultConfig); + } else { + // TODO + console.warn("No cluster config found"); + process.exit(1); + } + + this.entry = this._joinPath(this.entry || ""); + this.log.info = this._joinPath(this.log.info); + this.log.error = this._joinPath(this.log.error); + this.pid = this._joinPath(this.pid); + } + + // convert to absolute path + _joinPath(p) { + if(path.isAbsolute(p)) { + return p; + } + return path.join(this.root, p); + } +} + +module.exports = PMConfig; diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..e7b16c2 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,35 @@ +"use strict"; + +const yargs = require("yargs"); +const start = require("./start"); +const stop = require("./stop"); +const restart = require("./restart"); +const status = require("./stsuts"); + +// eslint-disable-next-line +yargs + .usage("$0 [options] ") + .help("h") + .alias("h", "help") + .alias("v", "version") + .command("start", "Start Akyuu.js Application in Production Mode", () => { + // EMPTY + }, argv => { + start(argv).then(() => { + process.exit(0); + }).catch(() => { + process.exit(1); + }); + }) + .command("stop", "Stop Akyuu.js Application", () => { + // EMPTY + }, stop) + .command("restart", "Restart Application", () => { + // EMPTY + }, restart) + .command("dev", "Start Akyuu.js Aplication in Development Mode") + .command("status", "Show Akyuu.js Application Status", () => { + // EMPTY + }, status) + .demandCommand() + .argv; diff --git a/lib/restart.js b/lib/restart.js new file mode 100644 index 0000000..3854301 --- /dev/null +++ b/lib/restart.js @@ -0,0 +1,8 @@ +const start = require("./start"); +const stop = require("./stop"); + +module.exports = function(argv) { + stop(argv).then(() => { + start(argv); + }); +}; diff --git a/lib/start.js b/lib/start.js new file mode 100644 index 0000000..e5425ab --- /dev/null +++ b/lib/start.js @@ -0,0 +1,61 @@ +"use strict"; + +const { spawn } = require("child_process"); +const fs = require("fs-extra"); +const PMConfig = require("./config"); +const path = require("path"); +const isRunning = require("is-running"); +const ora = require("ora"); + +module.exports = function() { + const spinner = ora("Starting process").start(); + const root = process.cwd(); + const config = new PMConfig(root); + const pidFile = config.pid; + + if(fs.pathExistsSync(pidFile)) { + const pid = parseInt(fs.readFileSync(pidFile, { encode: "utf-8" })); + if(pid && isRunning(pid)) { + spinner.info(`[${pid}] process already is running`); + process.exit(0); + } + } + + const env = Object.create(process.env); + env.NODE_CONFIG_DIR = path.join(root, "config"); + + const child = spawn("node", [ path.join(__dirname, "./cluster.js") ], { + detached: true, + env + }); + + let startFailed; + const promise = new Promise((resolve, reject) => { + startFailed = reject; + setTimeout(() => { + resolve(); + }, 5000); + }); + + child.on("exit", code => { + startFailed(new Error(`process exit with ${code}`)); + }); + + child.on("error", e => { + startFailed(e); + }); + + // child.stdout.pipe(process.stdout) + // child.stderr.pipe(process.stderr) + + fs.writeFileSync(pidFile, child.pid); + spinner.text = `[${child.pid}] process created, wating`; + + return promise.then(() => { + spinner.succeed(`[${child.pid}] process start success`); + child.unref(); + }).catch(e => { + spinner.fail(`[${child.pid}] process start failed`); + console.error(e); + }); +}; diff --git a/lib/stop.js b/lib/stop.js new file mode 100644 index 0000000..5f57505 --- /dev/null +++ b/lib/stop.js @@ -0,0 +1,47 @@ +"use strict"; + +const fs = require("fs-extra"); +const PMConfig = require("./config"); +const isRunning = require("is-running"); +const ora = require("ora"); + +module.exports = function() { + const spinner = ora("Stop process").start(); + const root = process.cwd(); + const config = new PMConfig(root); + + const pidFile = config.pid; + + if(fs.pathExistsSync(pidFile)) { + const pid = parseInt(fs.readFileSync(pidFile, { encode: "utf8" })); + if(isRunning(pid)) { + process.kill(pid); + return new Promise((resolve, reject) => { + let timeer = 0; + function check() { + if(isRunning(pid)) { + if(timeer > 25) { + reject(new Error()); + spinner.fail(`[${pid}] cannot stop process, please try again`); + } else { + // check process status every 200 ms + setTimeout(check, 200); + } + } else { + resolve(); + spinner.succeed(`[${pid}] stop process success`); + fs.removeSync(pidFile); + } + timeer++; + } + check(); + }); + } else { + spinner.info(`[${pid}] process stopped`); + } + } else { + spinner.warn(`no pid found: ${pidFile}`); + } + + return Promise.resolve(); +}; diff --git a/lib/stsuts.js b/lib/stsuts.js new file mode 100644 index 0000000..8003f83 --- /dev/null +++ b/lib/stsuts.js @@ -0,0 +1,24 @@ +"use strict"; + +const fs = require("fs-extra"); +const ora = require("ora"); +const PMConfig = require("./config"); +const isRunning = require("is-running"); + +module.exports = function() { + const spinner = ora("Checking").start(); + const root = process.cwd(); + const config = new PMConfig(root); + const pidFile = config.pid; + + if(fs.pathExistsSync(pidFile)) { + const pid = parseInt(fs.readFileSync(pidFile, { encode: "utf8" })); + if(isRunning) { + spinner.succeed(`[${pid}] process is running`); + } else { + spinner.info(`[${pid}] process is stopped`); + } + } else { + spinner.fail(`No pid file found: ${pidFile}`); + } +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..5d9435b --- /dev/null +++ b/package.json @@ -0,0 +1,38 @@ +{ + "name": "akyuu-bin", + "version": "0.0.0", + "description": "CLI tool for Akyuu.js.", + "main": "index.js", + "bin": { + "akyuu-bin": "./bin/akyuu-bin" + }, + "scripts": { + "dev": "node ./bin/akyuu-bin", + "lint": "eslint .", + "lint:fix": "eslint . --fix" + }, + "keywords": [ + "akyuu.js", + "akyuu", + "akyuu-bin", + "bin" + ], + "author": "XGHeaven", + "license": "MIT", + "devDependencies": { + "@types/yargs": "^10.0.0", + "eslint": "^4.13.1", + "eslint-config-xadillax-style": "^1.10.0", + "eslint-plugin-react": "^7.5.1" + }, + "dependencies": { + "fs-extra": "^5.0.0", + "is-running": "^2.1.0", + "lodash.defaultsdeep": "^4.6.0", + "ora": "^1.3.0", + "yargs": "^10.0.3" + }, + "peerDependencies": { + "akyuu": "^0.5.0" + } +} From 279112f9e0a8df7aaf3bff6988040d0aeb9698bf Mon Sep 17 00:00:00 2001 From: XGHeaven Date: Fri, 22 Dec 2017 12:33:24 +0800 Subject: [PATCH 2/3] dump version to v0.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5d9435b..059a505 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "akyuu-bin", - "version": "0.0.0", + "version": "0.1.0", "description": "CLI tool for Akyuu.js.", "main": "index.js", "bin": { From ddbc0a0c47b59d3f2109ca8b64b44372a2969dce Mon Sep 17 00:00:00 2001 From: XGHeaven Date: Fri, 22 Dec 2017 12:35:20 +0800 Subject: [PATCH 3/3] typo --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7d15808..d0eb8b4 100644 --- a/README.md +++ b/README.md @@ -52,8 +52,8 @@ CLI tool for Akyuu.js. { ... scripts: { - "start": "akyuu-pm start", - "stop": "akyuu-pm stop" + "start": "akyuu-bin start", + "stop": "akyuu-bin stop" } ... }