From d2313875ce3a7875b69e3e98256fed42e1b64338 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=A5=BA?= Date: Fri, 21 Dec 2018 15:36:06 +0800 Subject: [PATCH] n-api & `*Sync()` (#2) - Update to N-API - Add support for `GetACP` - Add asynchronously APIs --- .babelrc | 15 ------ .babelrc.js | 16 ++++++ .gitignore | 6 ++- .npmignore | 6 ++- README.md | 19 +++++-- appveyor.yml | 10 ++-- binding.gyp | 3 ++ package.json | 34 ++++++++----- src/binding.js | 16 +++++- src/fallback.js | 127 ++++++++++++++++++++++++++++++++--------------- src/index.js | 38 +++++++++++--- src/stdcp.cc | 89 +++++++++++++++++++++------------ test/binding.js | 38 +------------- test/fallback.js | 46 +---------------- test/polyfill.js | 4 ++ test/stdcp.js | 63 +++++++++++++++++++---- test/testcase.js | 72 +++++++++++++++++++++++++++ 17 files changed, 389 insertions(+), 213 deletions(-) delete mode 100644 .babelrc create mode 100644 .babelrc.js create mode 100644 test/polyfill.js create mode 100644 test/testcase.js diff --git a/.babelrc b/.babelrc deleted file mode 100644 index 82c5382..0000000 --- a/.babelrc +++ /dev/null @@ -1,15 +0,0 @@ -{ - "presets": [ - [ - "@babel/preset-env", - { - "targets" : { - "node": 4 - } - } - ] - ], - "plugins": [ - "@babel/plugin-transform-runtime" - ] -} diff --git a/.babelrc.js b/.babelrc.js new file mode 100644 index 0000000..94feaf2 --- /dev/null +++ b/.babelrc.js @@ -0,0 +1,16 @@ +"use strict"; +module.exports = { + "presets": [ + [ + "@babel/preset-env", + { + "targets": process.env.NYC_CONFIG ? {} : { + "node": 6, + }, + }, + ], + ], + "plugins": [ + "@babel/plugin-transform-runtime", + ], +}; diff --git a/.gitignore b/.gitignore index e9a85a3..c47f593 100644 --- a/.gitignore +++ b/.gitignore @@ -84,5 +84,7 @@ typings/ # End of https://www.gitignore.io/api/node -build -lib +*.tar.gz +build-tmp-napi-*/ +build/ +lib/ diff --git a/.npmignore b/.npmignore index 747cad5..a2d2fb9 100644 --- a/.npmignore +++ b/.npmignore @@ -1,6 +1,7 @@ *.log *.pid *.seed +*.tar.gz *.tgz .circleci .editorconfig @@ -24,4 +25,7 @@ npm-debug.log* pids test src/*.js -build +build/ +build-tmp-napi-*/ +lib/binding/**/* +!lib/binding/win32-x64-napi-v3/stdcp.node diff --git a/README.md b/README.md index dd451d6..5fe1a79 100644 --- a/README.md +++ b/README.md @@ -8,15 +8,28 @@ stdcp An effort to encapsulate the output code page used by the console of current process. This library provides native bindings for Windows APIs: -- [SetConsoleOutputCP](https://docs.microsoft.com/windows/console/setconsoleoutputcp) +- [GetACP](https://docs.microsoft.com/windows/desktop/api/winnls/nf-winnls-getacp) - [GetConsoleOutputCP](https://docs.microsoft.com/windows/console/getconsoleoutputcp) +- [SetConsoleOutputCP](https://docs.microsoft.com/windows/console/setconsoleoutputcp) ## Use Case ```javascript const stdcp = require("stdcp"); -stdcp.set(65001); -console.log(stdcp.get()) // 65001 + +// Asynchronously APIs +(async () => { + console.log(await stdcp.get(true)) // Get current Windows code page. + console.log(await stdcp.get()) // Get code page for current console. + await stdcp.set(65001); // Set code page for current console. +})(); + +// Synchronously APIs +(() => { + console.log(stdcp.getSync(true)) // Get current Windows code page. + console.log(stdcp.getSync()) // Get code page for current console. + stdcp.setSync(65001) // Set code page for current console. +})(); ``` ## Related diff --git a/appveyor.yml b/appveyor.yml index 8b717eb..7919370 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -6,7 +6,6 @@ environment: matrix: - nodejs_version: current - nodejs_version: lts - - nodejs_version: 8 - nodejs_version: 6 platform: @@ -18,19 +17,20 @@ install: # install Node.js - ps: Install-Product node $env:nodejs_version $env:platform # install modules - - npm install --build-from-source + - if %nodejs_version% LSS 8 npm i -g npm@6 + - npm install --build-from-source=stdcp # to run your custom scripts instead of automatic tests test_script: - - if not %nodejs_version% LSS 8 npm test - - npm run build + - npm test + - if "%nodejs_version%"=="lts" npm run build # to run your custom scripts instead of provider deployments after_test: - if not %nodejs_version% LSS 8 npm run report-coverage artifacts: - - path: 'build\stage\**\*.tar.gz' + - path: '**\*.tar.gz' name: binding deploy: diff --git a/binding.gyp b/binding.gyp index 471bdb7..c3de596 100644 --- a/binding.gyp +++ b/binding.gyp @@ -1,6 +1,9 @@ { # NOTE: 'module_name' and 'module_path' come from the 'binary' property in package.json # node-pre-gyp handles passing them down to node-gyp when you build from source + "defines": [ + "NAPI_VERSION=<(napi_build_version)" + ], "targets": [ { "target_name": "<(module_name)", diff --git a/package.json b/package.json index e45ecfc..80c95e8 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,11 @@ { "name": "stdcp", - "version": "1.0.0", + "version": "2.0.0", "description": "An effort to encapsulate the output code page used by the console of current process.", "keywords": [ - "SetConsoleOutputCP", + "GetACP", "GetConsoleOutputCP", + "SetConsoleOutputCP", "chcp", "console", "terminal", @@ -31,11 +32,13 @@ "node-pre-gyp": "^0.12.0" }, "devDependencies": { - "@babel/cli": "^7.2.0", - "@babel/core": "^7.2.0", + "@babel/cli": "^7.2.3", + "@babel/core": "^7.2.2", "@babel/plugin-transform-runtime": "^7.2.0", - "@babel/preset-env": "^7.2.0", + "@babel/preset-env": "^7.2.3", "@babel/register": "^7.0.0", + "@babel/runtime": "^7.2.0", + "chai": "^4.2.0", "codecov": "^3.1.0", "eclint": "^2.8.1", "eslint": "^5.10.0", @@ -44,17 +47,21 @@ "eslint-plugin-node": "^8.0.0", "eslint-plugin-promise": "^4.0.1", "eslint-plugin-standard": "^4.0.0", - "expect.js": "^0.3.1", "fs-extra": "^7.0.1", "mocha": "^5.2.0", "nyc": "^13.1.0" }, "binary": { "module_name": "stdcp", - "module_path": "./build/Release/binding/{node_abi}-{platform}-{arch}", - "package_name": "{node_abi}-{platform}-{arch}.tar.gz", + "module_path": "./lib/binding/{platform}-{arch}-{node_napi_label}", + "package_name": "{platform}-{arch}-{node_napi_label}.tar.gz", "host": "https://github.com/gucong3000/stdcp/releases/download/", - "remote_path": "v{version}" + "remote_path": "v{version}", + "napi_versions": [ + 1, + 2, + 3 + ] }, "nyc": { "reporter": [ @@ -65,11 +72,12 @@ }, "scripts": { "install": "node-pre-gyp install --fallback-to-build || echo fallback", - "build": "rm -rf lib && babel src --out-dir lib && node-pre-gyp rebuild --build-from-source && node-pre-gyp package", - "prepare": "npm run build", - "unit": "nyc mocha --no-timeouts", + "build:babel": "rm -rf lib/**/*.js && babel src --out-dir lib", + "build:gyp": "node-pre-gyp rebuild --build-from-source && node-pre-gyp package", + "build": "npm run build:babel && npm run build:gyp", + "unit": "nyc mocha --require test/polyfill --no-timeouts", "lint:eclint": "eclint check $(git ls-files | tee /tmp/git-files)", - "lint:eslint": "eslint $(grep \"\\.js$\" /tmp/git-files)", + "lint:eslint": "eslint --ignore-pattern ! $(grep \"\\.js$\" /tmp/git-files)", "lint": "npm run lint:eclint && npm run lint:eslint", "pretest": "env npm run lint --script-shell=/bin/sh", "test": "npm run unit", diff --git a/src/binding.js b/src/binding.js index 1048a3f..f357043 100644 --- a/src/binding.js +++ b/src/binding.js @@ -1,4 +1,18 @@ "use strict"; const binary = require("node-pre-gyp"); const bindingPath = binary.find(require.resolve("../package.json")); -module.exports = require(bindingPath); +const binding = require(bindingPath); + +binding.get = function get (global) { + return Promise.resolve().then(() => ( + binding.getSync(global) + )); +}; + +binding.set = function set (codepage) { + return Promise.resolve().then(() => ( + binding.setSync(codepage) + )); +}; + +module.exports = binding; diff --git a/src/fallback.js b/src/fallback.js index d2d1e8d..cc636c5 100644 --- a/src/fallback.js +++ b/src/fallback.js @@ -1,49 +1,94 @@ "use strict"; const childProcess = require("child_process"); const path = require("path"); -const chcpCom = path.join( - process.env.windir || process.env.SystemRoot || "C:/Windows", - "System32/chcp.com" -); -const stdout = process.stdout; -const key = "-ms-codepage"; - -function getCP () { - if (stdout[key]) { - return stdout[key]; - } - let cp = childProcess.spawnSync(chcpCom, { - stdio: [ - "inherit", - "pipe", - "ignore", - ], - env: {}, - encoding: "ascii", - }).stdout; - cp = +/\d+\s*$/.exec(cp)[0]; - stdout[key] = cp; - return cp; -} - -function setCP (codepage) { - if (stdout[key] && (stdout[key] === codepage)) { - return 0; - } - const status = childProcess.spawnSync("chcp", [String(codepage)], { - stdio: [ - "inherit", - "ignore", - "pipe", +const sysDir = path.join(process.env.windir || process.env.SystemRoot || "C:/Windows", "System32"); +const wmicExe = path.join(sysDir, "wbem/WMIC.exe"); +const chcpCom = path.join(sysDir, "chcp.com"); +const spawnOpts = { + stdio: [ + "inherit", + "pipe", + "ignore", + ], + env: {}, + encoding: "ascii", +}; + +function spawnAsync (args, callback) { + const child = childProcess.spawn(args.shift(), args, spawnOpts); + const stdout = []; + child.stdout.on("data", stdout.push.bind(stdout)); + child.on("close", code => { + // eslint-disable-next-line standard/no-callback-literal + callback({ + status: code || 0, + stdout: Buffer.concat(stdout).toString(spawnOpts.encoding), + }); + }); +} + +function spawnSync (args, callback) { + callback(childProcess.spawnSync(args.shift(), args, spawnOpts)); +} + +function getHelper (spawn, global, callback) { + spawn( + global + ? [wmicExe, "os", "get", "codeset"] + : [chcpCom], + result => { + const cp = +/\d+\s*$/.exec(result.stdout)[0]; + callback(cp); + } + ); +} + +function setHelper (spawn, codepage, callback) { + spawn( + [ + chcpCom, + String(codepage), ], - }).status; - if (!status) { - stdout[key] = codepage; - } - return status; + result => { + result = !result.status; + callback(result); + } + ); +} + +function cb2sync (fn, args) { + let rst; + fn.apply(this, args.concat(result => { + rst = result; + })); + return rst; +} + +function cb2promise (fn, args) { + return new Promise(resolve => { + fn.apply(this, args.concat(resolve)); + }); +} + +function get (global) { + return cb2promise(getHelper, [spawnAsync, global]); +} + +function getSync (global) { + return cb2sync(getHelper, [spawnSync, global]); +} + +function set (codepage) { + return cb2promise(setHelper, [spawnAsync, codepage]); +} + +function setSync (codepage) { + return cb2sync(setHelper, [spawnSync, codepage]); } module.exports = { - set: setCP, - get: getCP, + get, + set, + getSync, + setSync, }; diff --git a/src/index.js b/src/index.js index d441f3d..492cb44 100644 --- a/src/index.js +++ b/src/index.js @@ -6,15 +6,37 @@ try { binding = require("./fallback"); } -function setCP (codepage) { - if (typeof codepage !== "number") { - throw new TypeError("Code page should be a integer."); - } - if (!Number.isSafeInteger(codepage) || codepage <= 0 || codepage >> 16 || binding.set(codepage)) { +function invalidCode (result) { + if (result) { + if (result.then) { + return result.then(invalidCode); + } + } else { throw new RangeError("Invalid code page."); } } -module.exports = Object.assign({}, binding, { - set: setCP, -}); +function setter (setFn) { + return function set (codepage) { + if (!Number.isSafeInteger(codepage) || codepage < 0 || codepage > 0xffff) { + throw new TypeError("`codepage` must be an unsigned integer."); + } + return invalidCode(setFn(codepage)); + }; +} + +function getter (getFn) { + return function get (global) { + if (global != null && typeof global !== "boolean") { + throw new TypeError("`global` should be a boolean."); + } + return getFn(global); + }; +} + +module.exports = { + get: getter(binding.get), + set: setter(binding.set), + getSync: getter(binding.getSync), + setSync: setter(binding.setSync), +}; diff --git a/src/stdcp.cc b/src/stdcp.cc index d4e2f03..bb040e3 100644 --- a/src/stdcp.cc +++ b/src/stdcp.cc @@ -1,41 +1,64 @@ -#include #include -namespace stdcp { - -using v8::FunctionCallbackInfo; -using v8::Isolate; -using v8::Local; -using v8::Number; -using v8::Object; -using v8::Value; - -void GetCP(const FunctionCallbackInfo& args) { - args.GetReturnValue().Set( - Number::New( - args.GetIsolate(), - GetConsoleOutputCP() - ) - ); -} +#include +#include + +napi_value GetCP( + napi_env env, + napi_callback_info info +) { + napi_status status; -void SetCP(const FunctionCallbackInfo& args) { - int error = 0; - if(!SetConsoleOutputCP(args[0].As()->Value())){ - error = GetLastError(); + size_t argc = 1; + napi_value args[1]; + status = napi_get_cb_info(env, info, &argc, args, nullptr, nullptr); + bool global = status == napi_ok && argc >= 1; + if (global) { + status = napi_get_value_bool(env, args[0], &global); + global = status == napi_ok && global; } - args.GetReturnValue().Set( - Number::New( - args.GetIsolate(), - error - ) - ); + + napi_value code; + // https://docs.microsoft.com/windows/desktop/api/winnls/nf-winnls-getacp + // https://docs.microsoft.com/windows/console/getconsoleoutputcp + status = napi_create_int32(env, global ? GetACP() : GetConsoleOutputCP(), &code); + assert(status == napi_ok); + return code; } -void Init(Local exports) { - NODE_SET_METHOD(exports, "get", GetCP); - NODE_SET_METHOD(exports, "set", SetCP); +napi_value SetCP( + napi_env env, + napi_callback_info info +) { + napi_status status; + + size_t argc = 1; + napi_value args[1]; + status = napi_get_cb_info(env, info, &argc, args, nullptr, nullptr); + bool result = status == napi_ok && argc >= 1; + if (result) { + int code; + status = napi_get_value_int32(env, args[0], &code); + // https://docs.microsoft.com/windows/console/setconsoleoutputcp + result = status == napi_ok && SetConsoleOutputCP(code); + } + + napi_value napiResult; + status = napi_get_boolean(env, result, &napiResult); + assert(status == napi_ok); + return napiResult; } -NODE_MODULE(NODE_GYP_MODULE_NAME, Init) +#define DECLARE_NAPI_METHOD(name, func) { name, 0, func, 0, 0, 0, (napi_property_attributes) (napi_configurable | napi_enumerable | napi_writable), 0 } + +napi_value Init(napi_env env, napi_value exports) { + napi_status status; + napi_property_descriptor properties[] = { + DECLARE_NAPI_METHOD("getSync", GetCP), + DECLARE_NAPI_METHOD("setSync", SetCP), + }; + status = napi_define_properties(env, exports, 2, properties); + assert(status == napi_ok); + return exports; +} -} // namespace stdcp +NAPI_MODULE(NODE_GYP_MODULE_NAME, Init) diff --git a/test/binding.js b/test/binding.js index cc08b5e..56ce644 100644 --- a/test/binding.js +++ b/test/binding.js @@ -1,38 +1,4 @@ "use strict"; -const binding = require("../src/binding"); -const expect = require("expect.js"); +const testcase = require("./testcase"); -describe("binding", () => { - const codepage = binding.get(); - - it("binding.get()", () => { - expect(typeof codepage).to.equal("number"); - }); - - it("binding.set(\"999\")", () => { - expect(binding.set("999")).to.greaterThan(0); - }); - - it("binding.set()", () => { - expect(binding.set()).to.greaterThan(0); - }); - - it("binding.set(999999)", () => { - expect(binding.set(999999)).to.greaterThan(0); - }); - - [ - 65001, - 850, - 437, - codepage, - ].filter(Boolean).forEach((code) => { - it(`binding.set(${code})`, () => { - binding.set(code); - }); - - it(`binding.get(${code})`, () => { - expect(binding.get()).to.equal(code); - }); - }); -}); +testcase("binding"); diff --git a/test/fallback.js b/test/fallback.js index 4354e40..55e32b3 100644 --- a/test/fallback.js +++ b/test/fallback.js @@ -1,46 +1,4 @@ "use strict"; -const fallback = require("../src/fallback"); -const binding = require("../src/binding"); -const expect = require("expect.js"); +const testcase = require("./testcase"); -describe("fallback", () => { - const codepage = fallback.get(); - - it("fallback.get()", () => { - expect(codepage).to.equal(binding.get()); - }); - - it("fallback.set(\"999\")", () => { - expect(fallback.set("999")).to.greaterThan(0); - }); - - it("fallback.set()", () => { - expect(fallback.set()).to.greaterThan(0); - }); - - it("fallback.set(999999)", () => { - expect(fallback.set(999999)).to.greaterThan(0); - }); - - [ - 65001, - 850, - 437, - codepage, - ].filter(Boolean).forEach((code) => { - it(`fallback.set(${code})`, () => { - fallback.set(code); - }); - - it(`fallback.set(${code}) with timeout`, () => { - const timer = Date.now(); - fallback.set(code); - expect(Date.now() - timer).to.lessThan(9); - }); - - it(`fallback.get(${code})`, () => { - expect(fallback.get()).to.equal(code); - expect(code).to.equal(binding.get()); - }); - }); -}); +testcase("fallback"); diff --git a/test/polyfill.js b/test/polyfill.js new file mode 100644 index 0000000..877981c --- /dev/null +++ b/test/polyfill.js @@ -0,0 +1,4 @@ +"use strict"; +if (parseInt(process.versions.v8) < 6) { + require("@babel/register"); +} diff --git a/test/stdcp.js b/test/stdcp.js index 84921bb..a4ee7c5 100644 --- a/test/stdcp.js +++ b/test/stdcp.js @@ -1,54 +1,95 @@ "use strict"; const stdcp = require("../src"); -const fallback = require("../src/fallback"); -const expect = require("expect.js"); +const expect = require("chai").expect; const binary = require("node-pre-gyp"); +const util = require("util"); describe("stdcp", () => { [ "8848", + Math.PI, {}, () => {}, null, undefined, true, false, + "", + -1, + 0xFFFFFFFF, ].forEach(value => { - it(`stdcp.set(${value})`, () => { - expect(stdcp.set).withArgs(value).to.throwException(/^Code page should be a integer\.$/); + it(`stdcp.setSync(${util.inspect(value)})`, () => { + expect(() => { + stdcp.setSync(value); + }).to.throw("`codepage` must be an unsigned integer."); }); }); + [ + "8848", + {}, + () => {}, + "", -1, Math.PI, 600, 0xFFFFFFFF, ].forEach(value => { - it(`stdcp.set(${value})`, () => { - expect(stdcp.set).withArgs(value).to.throwException(/^Invalid code page\.$/); + it(`stdcp.getSync(${util.inspect(value)})`, () => { + expect(() => { + stdcp.getSync(value); + }).to.throw("`global` should be a boolean."); + }); + }); + + [ + 600, + 999, + ].forEach(value => { + it(`stdcp.setSync(${value})`, () => { + expect(() => { + stdcp.setSync(value); + }).to.throw("Invalid code page."); + }); + + it(`stdcp.set(${value})`, async () => { + let error; + try { + await stdcp.set(value); + } catch (ex) { + error = ex; + } + expect(error).to.have.property("message", "Invalid code page."); }); }); - it("stdcp.set(stdcp.get())", () => { - const codepage = stdcp.get(); + it("stdcp.setSync(stdcp.getSync())", () => { + const codepage = stdcp.getSync(false); expect(typeof codepage).to.equal("number"); - stdcp.set(codepage); + stdcp.setSync(codepage); }); }); describe("fallback to js", () => { let binFind; + let envBak; before(() => { delete require.cache[require.resolve("../src")]; delete require.cache[require.resolve("../src/binding")]; + delete require.cache[require.resolve("../src/fallback")]; + envBak = Object.assign({}, process.env); binFind = binary.find; }); after(() => { binary.find = binFind; + process.env = Object.assign(process.env, envBak); }); it("binary.find = null", () => { binary.find = null; - const stdcp = require("../src"); - expect(stdcp.get).to.equal(fallback.get); + delete process.env.windir; + delete process.env.SystemRoot; + require("../src"); + expect(require.cache[require.resolve("../src/binding")]).to.equal(undefined); + expect(require.cache[require.resolve("../src/fallback")]).to.have.property("loaded", true); }); }); diff --git a/test/testcase.js b/test/testcase.js new file mode 100644 index 0000000..5cb8461 --- /dev/null +++ b/test/testcase.js @@ -0,0 +1,72 @@ +"use strict"; +const expect = require("chai").expect; + +function testcase (id) { + const fallbackId = id === "binding" ? "fallback" : "binding"; + const module = require("../src/" + id); + const fallback = require("../src/" + fallbackId); + describe(id, () => { + const codepage = module.getSync(true); + + it(id + ".getSync(true)", () => { + expect(codepage).to.be.a("number"); + expect(codepage).to.equal(fallback.getSync(true)); + }); + + it(id + ".get(true)", async () => { + expect(await module.get(true)).to.equal(codepage); + }); + + it(id + ".getSync()", () => { + const codepage = module.getSync(); + expect(codepage).to.be.a("number"); + }); + + it(id + ".get()", async () => { + expect(await module.get()).to.equal(module.getSync()); + }); + + it(id + ".setSync(\"999\")", () => { + expect(module.setSync("999")).to.to.equal(false); + }); + + it(id + ".setSync()", () => { + expect(module.setSync()).to.to.equal(false); + }); + + it(id + ".setSync(999999)", () => { + expect(module.setSync(999999)).to.to.equal(false); + }); + + [ + 65001, + 850, + 437, + codepage, + ].filter(Boolean).forEach((code) => { + it(`${id}.setSync(${code}) => true`, () => { + expect(module.setSync(code)).to.equal(true); + }); + + it(`${id}.set(${code}) => true`, async () => { + expect(await module.set(code)).to.equal(true); + }); + + it(`${id}.getSync() => ${code}`, () => { + expect(module.getSync()).to.equal(code); + }); + + it(`${id}.get() => ${code}`, async () => { + expect(await module.get()).to.equal(code); + }); + + if (id === "fallback") { + it(`${fallbackId}.getSync() => ${code}`, () => { + expect(fallback.getSync()).to.equal(code); + }); + } + }); + }); +} + +module.exports = testcase;