├── AUTHORS
├── .gitignore
├── .npmignore
├── .travis.yml
├── test
├── mocha.opts
├── mock
│ ├── errorDemo.js
│ ├── asyncErrorDemo.js
│ ├── timeoutDemo.js
│ ├── asyncCorrectDemo.js
│ ├── correctDemo.js
│ └── cli.mock.js
├── helper
│ └── utils.js
├── cli.test.js
└── lib.test.js
├── doc
├── postman-step1.png
├── postman-step2.png
└── postman-step3.png
├── lib
└── debug
│ ├── config
│ ├── index.js
│ ├── config.default.js
│ └── config.development.js
│ ├── widget
│ └── slogan.js
│ ├── middlewares
│ ├── extendForCtx.js
│ └── bootstrap.js
│ ├── lib
│ ├── logger.js
│ └── wrapper.js
│ ├── bootstrap.js
│ └── helper
│ └── utils.js
├── .editorconfig
├── .eslintrc
├── LICENSE
├── package.json
├── README.md
├── README_en.md
├── bin
└── index.js
└── CHANGELOG.md
/AUTHORS:
--------------------------------------------------------------------------------
1 | louiswu <574637316@qq.com>
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | yarn-error.log
3 | yarn.lock
4 | demo.js
5 | coverage/
6 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | yarn-error.log
3 | yarn.lock
4 | demo.js
5 | coverage/
6 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - '9.8.0'
4 | script:
5 | - npm run test
6 |
--------------------------------------------------------------------------------
/test/mocha.opts:
--------------------------------------------------------------------------------
1 | ###
2 | ### mocha.opts
3 | ###
4 |
5 | --require should
6 | --reporter spec
7 |
--------------------------------------------------------------------------------
/doc/postman-step1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TencentCloud/scf-node-debug/HEAD/doc/postman-step1.png
--------------------------------------------------------------------------------
/doc/postman-step2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TencentCloud/scf-node-debug/HEAD/doc/postman-step2.png
--------------------------------------------------------------------------------
/doc/postman-step3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TencentCloud/scf-node-debug/HEAD/doc/postman-step3.png
--------------------------------------------------------------------------------
/lib/debug/config/index.js:
--------------------------------------------------------------------------------
1 | module.exports = process.env === 'production'
2 | ? require('./config.production')
3 | : require('./config.development')
--------------------------------------------------------------------------------
/test/mock/errorDemo.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | exports.main_handler = (event, context, callback) => {
3 | throw Error('error')
4 | return 'sync-error'
5 | }
6 |
--------------------------------------------------------------------------------
/test/mock/asyncErrorDemo.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | exports.main_handler = async (event, context, callback) => {
3 | throw Error('error')
4 | return 'async-error'
5 | }
6 |
--------------------------------------------------------------------------------
/test/mock/timeoutDemo.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | exports.main_handler = (event, context, callback) => {
3 | setTimeout(() => {
4 | console.log('test')
5 | }, 4000)
6 | return 'test123'
7 | }
8 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 | [**]
3 | end_of_line = lf
4 | charset = utf-8
5 | insert_final_newline = true
6 | [!node_modules]
7 | indent_style = space
8 | indent_size = 2
9 | trim_trailing_whitespace = true
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parserOptions": {
3 | "ecmaVersion": 8,
4 | "ecmaFeatures": {
5 | "experimentalObjectRestSpread": true
6 | }
7 | },
8 | "rules": {
9 | "semi": ["error", "never"],
10 | "quotes": ["error", "single"]
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/test/mock/asyncCorrectDemo.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const delay = (interval = 1000) => {
3 | return new Promise(resolve => {
4 | setTimeout(resolve, interval)
5 | })
6 | }
7 |
8 | exports.main_handler = async (event, context, callback) => {
9 | await delay()
10 | return 'async-correct'
11 | }
12 |
--------------------------------------------------------------------------------
/test/mock/correctDemo.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const delay = (interval = 1000) => {
4 | return new Promise(resolve => {
5 | setTimeout(resolve, interval)
6 | })
7 | }
8 |
9 | exports.main_handler = (event, context, callback) => {
10 | delay().then(res => {
11 | callback(null, 'sync-correct')
12 | })
13 | return 'sync-correct'
14 | }
15 |
--------------------------------------------------------------------------------
/lib/debug/widget/slogan.js:
--------------------------------------------------------------------------------
1 | module.exports =
2 | `
3 | ███████╗ ██████╗███████╗ ██████╗██╗ ██╗
4 | ██╔════╝██╔════╝██╔════╝ ██╔════╝██║ ██║
5 | ███████╗██║ █████╗ ██║ ██║ ██║
6 | ╚════██║██║ ██╔══╝ ██║ ██║ ██║
7 | ███████║╚██████╗██║ ╚██████╗███████╗██║
8 | ╚══════╝ ╚═════╝╚═╝ ╚═════╝╚══════╝╚═╝
9 | `
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Tencent Cloud
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/lib/debug/config/config.default.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | testModel: {
3 | http: null,
4 | apigateway: {
5 | requestContext: {
6 | serviceName: 'testsvc',
7 | path: '/test/{path}',
8 | httpMethod: 'POST',
9 | requestId: 'c6af9ac6-7b61-11e6-9a41-93e8deadbeef',
10 | identity: {
11 | secretId: 'abdcdxxxxxxxsdfs'
12 | },
13 | sourceIp: '10.0.2.14',
14 | stage: 'prod'
15 | },
16 | headers: {
17 | 'Accept-Language': 'en-US,en,cn',
18 | Accept: 'text/html,application/xml,application/json',
19 | Host: 'service-3ei3tii4-251000691.ap-guangzhou.apigateway.myqloud.com',
20 | 'User-Agent': 'User Agent String'
21 | },
22 | body: '{"test":"body"}',
23 | pathParameters: {
24 | path: 'value'
25 | },
26 | queryStringParameters: {
27 | foo: 'bar'
28 | },
29 | headerParameters: {
30 | Refer: '10.0.2.14'
31 | },
32 | stageVariables: {
33 | stage: 'test'
34 | },
35 | path: '/test/value?foo=bar',
36 | httpMethod: 'POST'
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/lib/debug/middlewares/extendForCtx.js:
--------------------------------------------------------------------------------
1 | const utils = require('../helper/utils')
2 |
3 | module.exports = function({ eventType = 'http' }) {
4 | return async function(ctx, next) {
5 | // 扩展requestId
6 | ctx.requestId = utils.guid()
7 | // 扩展testModel
8 | ctx.testModel = utils.generateEvent(ctx, eventType)
9 | // 重写toJSON,返回更多参数
10 | if (eventType === 'http') {
11 | const tmpRequestStringify = JSON.stringify(ctx.testModel.request)
12 | ctx.testModel.request.toJSON = () => {
13 | return Object.assign(JSON.parse(tmpRequestStringify), {
14 | body: ctx.request.body,
15 | files: ctx.request.files
16 | })
17 | }
18 |
19 | const tmpCtxStringify = JSON.stringify(ctx.testModel)
20 | ctx.testModel.toJSON = () => {
21 | return Object.assign(JSON.parse(tmpCtxStringify), {
22 | request: Object.assign(JSON.parse(tmpRequestStringify), {
23 | body: ctx.request.body
24 | }),
25 | requestId: ctx.requestId,
26 | query: ctx.query,
27 | // for koa-body
28 | body: ctx.request.body,
29 | files: ctx.request.files
30 | })
31 | }
32 | }
33 | await next()
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/test/mock/cli.mock.js:
--------------------------------------------------------------------------------
1 | const testModelMap = ['http', 'cmq', 'ckafka', 'apigateway', 'helloworld']
2 | const data = {
3 | entry: {
4 | defaultData: '',
5 | stdinData: './correctDemo.js'
6 | },
7 | handler: {
8 | defaultData: 'main_handler',
9 | stdinData: 'main'
10 | },
11 | timeout: {
12 | defaultData: 3,
13 | stdinData: 5
14 | },
15 | testModel: {
16 | defaultData: testModelMap[0],
17 | stdinData: ''
18 | }
19 | }
20 | const promps = [
21 | {
22 | type: 'input',
23 | name: 'entry',
24 | message: '请输入入口文件地址(相对路径)',
25 | validate: function(input) {
26 | if (!input) {
27 | return '请输入入口文件地址'
28 | }
29 | return true
30 | }
31 | },
32 | {
33 | type: 'input',
34 | name: 'handler',
35 | default: 'main_hanlder',
36 | message: '请输入入口执行方法名称',
37 | validate: function(input) {
38 | return true
39 | }
40 | },
41 | {
42 | type: 'input',
43 | name: 'timeout',
44 | default: 3,
45 | message: '请输入超时时间限制(单位:s)',
46 | validate: function(input) {
47 | if (+input <= 30 && +input >= 1) {
48 | return true
49 | }
50 | return '请输入1-30s的数字'
51 | }
52 | },
53 | {
54 | type: 'list',
55 | name: 'testModel',
56 | choices: testModelMap,
57 | message: '请选择测试模版',
58 | validate: function(input) {
59 | return true
60 | }
61 | }
62 | ]
63 |
64 | module.exports = {
65 | testModelMap, // 测试模型
66 | data, // 测试数据,包括默认数据和命令行输入数据
67 | promps // 交互式命令行
68 | }
69 |
--------------------------------------------------------------------------------
/test/helper/utils.js:
--------------------------------------------------------------------------------
1 | // 偏函数
2 | const _isType = function(type) {
3 | const typeMap = [
4 | 'Number',
5 | 'String',
6 | 'Function',
7 | 'Object',
8 | 'Null',
9 | 'Undefined',
10 | 'Symbol',
11 | 'Array'
12 | ]
13 | if (typeMap.indexOf(type) === -1) throw `传入类型不正确,可选${typeMap}`
14 | return obj => {
15 | return toString.call(obj) === `[object ${type}]`
16 | }
17 | }
18 |
19 | module.exports = {
20 | isObject: _isType('Object'),
21 | /**
22 | * 从对象中拉取对应的属性值
23 | * @param {*} object
24 | */
25 | getEntityByObject(object) {
26 | let selectedItem = ''
27 | let resObj = {}
28 |
29 | function _generate() {
30 | for (let key in object) {
31 | if (object.hasOwnProperty(key)) {
32 | resObj[key] = object[key][selectedItem]
33 | }
34 | }
35 | return resObj
36 | }
37 |
38 | function _value(key) {
39 | selectedItem = key
40 | return _generate()
41 | }
42 |
43 | return { value: _value }
44 | },
45 |
46 | /**
47 | * 对比输入的数据和输出的数据,看是否符合预期;输入为空则使用默认值,输入不为空则使用输入值
48 | * @param {*} defaultData
49 | * @param {*} stdinData
50 | * @param {*} answers
51 | */
52 | validate(defaultData, stdinData, answers) {
53 | let isValid = 1
54 | for (let item in stdinData) {
55 | // 输入不为空
56 | if (stdinData[item]) {
57 | // 是否与输出一致
58 | if (!stdinData[item] === answers[item]) {
59 | isValid = !1
60 | break
61 | }
62 | }
63 | // 输入为空
64 | else {
65 | // 是否与默认值一致
66 | if (!answers[item] === defaultData[item]) {
67 | isValid = !1
68 | break
69 | }
70 | }
71 | }
72 | return isValid
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "scf-cli",
3 | "version": "1.0.8",
4 | "main": "./bin/index.js",
5 | "author": "louiswu",
6 | "license": "MIT",
7 | "dependencies": {
8 | "chalk": "^2.4.1",
9 | "child_process": "^1.0.2",
10 | "cli-spinner": "^0.2.8",
11 | "colors": "^1.3.0",
12 | "commander": "^2.15.1",
13 | "figlet": "^1.2.0",
14 | "inquirer": "^6.0.0",
15 | "koa": "^2.5.1",
16 | "koa-body": "^4.0.4",
17 | "lodash": "^4.17.10",
18 | "memeye": "^1.0.3",
19 | "moment": "^2.22.2",
20 | "ora": "^2.1.0",
21 | "supervisor": "^0.12.0",
22 | "tty-table": "^2.6.8"
23 | },
24 | "scripts": {
25 | "dev": "supervisor ./bootstrap.js",
26 | "cover": "istanbul cover _mocha -- test/*.test.js -R spec",
27 | "test": "mocha test/*.js",
28 | "release": "standard-version",
29 | "pub": "git push --follow-tags origin master && npm publish"
30 | },
31 | "bin": {
32 | "scf": "./bin/index.js"
33 | },
34 | "engines": {
35 | "node": ">= 8.1.4"
36 | },
37 | "description": "本地测试运行云函数的小工具",
38 | "devDependencies": {
39 | "assert": "^1.4.1",
40 | "bdd-stdin": "^0.2.0",
41 | "chai": "^4.1.2",
42 | "command-line-test": "^1.0.10",
43 | "commitizen": "^2.10.1",
44 | "cz-conventional-changelog": "^2.1.0",
45 | "eslint": "^6.6.0",
46 | "istanbul": "^1.0.0-alpha.2",
47 | "mocha": "^5.2.0",
48 | "request": "^2.88.0",
49 | "request-promise-any": "^1.0.5",
50 | "should": "^13.2.1",
51 | "standard-version": "^4.4.0",
52 | "supertest": "^3.1.0"
53 | },
54 | "config": {
55 | "commitizen": {
56 | "path": "./node_modules/cz-conventional-changelog"
57 | }
58 | },
59 | "keywords": [
60 | "serverless cloud function",
61 | "cli",
62 | "debug",
63 | "Tencent Cloud"
64 | ]
65 | }
66 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SCF CLI
2 |
3 | [](./LICENSE)
4 | [](https://www.npmjs.com/package/scf-cli)
5 | [](https://www.npmjs.com/package/scf-cli)
6 | [](https://travis-ci.org/TencentCloud/scf-node-debug.svg?branch=master)
7 |
8 | 这是一个用于本地测试运行云函数的小工具,我们提供了几种测试模型作为云函数的入参'event'。
9 | 本工具需要 node8.1.4 以上版本以支持 ES2015,async function 和 koa。
10 |
11 | [English DOC](https://github.com/TencentCloud/scf-node-debug/blob/master/README_en.md)
12 | [中文版文档](https://github.com/TencentCloud/scf-node-debug/blob/master/README.md)
13 |
14 | ## Installation
15 |
16 | ```
17 | npm install scf-cli -g
18 | ```
19 |
20 | ## Quick Start
21 |
22 | ```
23 | scf init
24 | ```
25 |
26 | ### Options
27 |
28 | - host 用于开启本地服务的 host
29 | - port 用于开启本地服务的 port
30 | - debug 开启调试模式。一旦开启,你将在 bash 控制台看到关于云函数运行的详情,比如错误信息、错误码、返回内容
31 |
32 | ### Command Line
33 |
34 | - init 初始化调试工具,包括入参、测试模型等
35 |
36 | ### Configurations
37 |
38 | - entry 云函数的入口文件
39 | - handler 云函数入口文件的执行方法,调试工具内部会选择该执行方法作为
40 | - timeout 云函数的超时时间,用于控制函数执行时间
41 | - testModel 选择云函数的入参'event'的模式
42 | - http 和公有云函数标准一致
43 | - apigateway 以 apigateway 的模式模拟入参
44 | - helloworld 入参为最简单的 json 格式
45 | - cmq 以 cmq 的模式模拟入参
46 | - ckafka 以 ckafka 的模式模拟入参
47 |
48 | ## 常见问题
49 |
50 | - [如何设置本地 web 代理?](https://github.com/TencentCloud/scf-node-debug/wiki/%E5%A6%82%E4%BD%95%E8%AE%BE%E7%BD%AE%E6%9C%AC%E5%9C%B0web%E4%BB%A3%E7%90%86%EF%BC%9F)
51 | - 本地云函数的环境变量如何设置?(云函数在运行时,已经拥有了命令行启动所在进程空间的所有环境变量。)
52 | - [如何调试云函数的运行?](https://github.com/TencentCloud/scf-node-debug/wiki/%E5%A6%82%E4%BD%95%E8%B0%83%E8%AF%95%E4%BA%91%E5%87%BD%E6%95%B0%E7%9A%84%E8%BF%90%E8%A1%8C%EF%BC%9F)
53 | - [如何本地测试更多不同语言编写的云函数?](https://github.com/tencentyun/scfcli)
54 |
55 | ## TODO List
56 |
57 | - 本地更新、部署、管理云函数
58 | - 本地启用 docker 来测试云函数,让本地测试和云上运行保持更高的一致性
59 |
60 | ## Licence
61 |
62 | MIT
63 |
--------------------------------------------------------------------------------
/lib/debug/lib/logger.js:
--------------------------------------------------------------------------------
1 | const colors = require('colors/safe')
2 | const moment = require('moment')
3 |
4 | colors.setTheme({
5 | info: 'green',
6 | warn: 'yellow',
7 | debug: 'cyan',
8 | error: 'red'
9 | })
10 |
11 | const levelMap = {
12 | error: 0,
13 | warn: 1,
14 | info: 2,
15 | debug: 3
16 | }
17 |
18 | Object.freeze(levelMap)
19 |
20 | const DEFAULT_PREFIX = '[Weapp CLI]'
21 |
22 | function Logger(level = 2, prefix = DEFAULT_PREFIX) {
23 | this.setLevel.call(this, level)
24 | this.prefix = prefix
25 | this.levelMap = levelMap
26 | }
27 |
28 | function now() {
29 | return `[${moment().format()}] `
30 | }
31 |
32 | Logger.prototype.setLevel = function(level = 2) {
33 | return (this.level = level)
34 | }
35 |
36 | Logger.prototype.setPrefix = function(prefix = DEFAULT_PREFIX) {
37 | return (this.prefix = prefix)
38 | }
39 |
40 | Logger.prototype.error = function(msg, pureText) {
41 | if (this.level >= levelMap['error']) {
42 | if (pureText) {
43 | return this.prefix + now() + colors['error'](msg)
44 | } else {
45 | console.log(this.prefix + now() + colors['error'](msg))
46 | }
47 | }
48 | }
49 |
50 | Logger.prototype.warn = function(msg, pureText) {
51 | if (this.level >= levelMap['warn']) {
52 | if (pureText) {
53 | return this.prefix + now() + colors['warn'](msg)
54 | } else {
55 | console.log(this.prefix + now() + colors['warn'](msg))
56 | }
57 | }
58 | }
59 |
60 | Logger.prototype.info = function(msg, pureText) {
61 | if (this.level >= levelMap['info']) {
62 | if (pureText) {
63 | return this.prefix + now() + colors['info'](msg)
64 | } else {
65 | console.log(this.prefix + now() + colors['info'](msg))
66 | }
67 | }
68 | }
69 |
70 | Logger.prototype.debug = function(msg, pureText) {
71 | if (this.level >= levelMap['debug']) {
72 | if (pureText) {
73 | return this.prefix + now() + colors['debug'](msg)
74 | } else {
75 | console.log(this.prefix + now() + colors['debug'](msg))
76 | }
77 | }
78 | }
79 |
80 | Logger.prototype.now = function() {
81 | return now()
82 | }
83 |
84 | module.exports = new Logger(2)
85 |
--------------------------------------------------------------------------------
/README_en.md:
--------------------------------------------------------------------------------
1 | # SCF CLI
2 |
3 | [](./LICENSE)
4 | [](https://www.npmjs.com/package/scf-cli)
5 | [](https://www.npmjs.com/package/scf-cli)
6 |
7 | A cli For Test Your ServerLess Function
8 | We provide serveral testModels for you to config the 'event' of Your ServerLess Function
9 | And more properties is coming!
10 | This cli requires node v8.1.4 or higher for ES2015 , async function support and koa.
11 |
12 | [English DOC](https://github.com/TencentCloud/scf-node-debug/blob/master/README_en.md)
13 | [中文版文档](https://github.com/TencentCloud/scf-node-debug/blob/master/README.md)
14 |
15 | ## Installation
16 |
17 | ```
18 | npm install scf-cli -g
19 | ```
20 |
21 | ## Quick Start
22 |
23 | ```
24 | scf init
25 | ```
26 |
27 | ### Options
28 |
29 | - host The host To Start The Mock Server
30 | - port The port To Start The Mock Server
31 | - debug Start Supervisor.If set 'debug' to true, you would get the debug message of the MockServer from the bash.Or the MockServer would run as a backend process
32 |
33 | ### Command Line
34 |
35 | - init Init The TestCli For Your App
36 |
37 | ### Configurations
38 |
39 | - entry The entry of Your ServerLess Function File
40 | - handler The handler of Your ServerLess Function File
41 | - timeout Set the timeout For Your ServerLess Function. To control the execution time
42 | - testModel To choose a testModel as the 'event' For Your ServerLess Function
43 | - http
44 | - apigateway
45 | - helloworld
46 | - cmq
47 | - ckafka
48 |
49 | ## 常见问题
50 |
51 | - [How to config web-proxy?](https://github.com/TencentCloud/scf-node-debug/wiki/%E5%A6%82%E4%BD%95%E8%AE%BE%E7%BD%AE%E6%9C%AC%E5%9C%B0web%E4%BB%A3%E7%90%86%EF%BC%9F)
52 | - How to set the env locally?(The scf locally would have all env of the process which is running `scf-cli`)
53 | - [How to debug?](https://github.com/TencentCloud/scf-node-debug/wiki/%E5%A6%82%E4%BD%95%E8%B0%83%E8%AF%95%E4%BA%91%E5%87%BD%E6%95%B0%E7%9A%84%E8%BF%90%E8%A1%8C%EF%BC%9F)
54 |
55 | ## TODO List
56 |
57 | - Update Your ServerLess Function Localy
58 | - Test Your ServerLess Function Localy in docker
59 |
60 | ## Licence
61 |
62 | MIT
63 |
--------------------------------------------------------------------------------
/lib/debug/bootstrap.js:
--------------------------------------------------------------------------------
1 | // external depend
2 | const koa = require('koa')
3 | const path = require('path')
4 | const bodyParser = require('koa-body')
5 |
6 | // internal depend
7 | const utils = require('./helper/utils')
8 | const logger = require('./lib/logger')
9 | const config = require('./config')
10 | const scfConfig = config.scfConfig
11 | const bootstrap = require('./middlewares/bootstrap')
12 | const extendForCtx = require('./middlewares/extendForCtx')
13 |
14 | module.exports = {
15 | init(opts, cb) {
16 | const app = new koa()
17 | // 入口文件配置
18 | let entryOptions = {
19 | host: config.host,
20 | port: config.port,
21 | scfConfig
22 | }
23 | if (opts && opts.scfConfig) {
24 | entryOptions = Object.assign(entryOptions, opts)
25 | }
26 |
27 | /**************************************************
28 | middleWares
29 | ***************************************************/
30 | // error Control
31 | app.use(async (ctx, next) => {
32 | try {
33 | await next()
34 | if (!utils.hasValue(ctx.body)) {
35 | ctx.status = 404
36 | ctx.body = {
37 | code: -1,
38 | message: 'no content body',
39 | data: null
40 | }
41 | } else {
42 | ctx.status = 200
43 | }
44 | } catch (e) {
45 | logger.error(e && e.toString && e.toString())
46 | ctx.body = {
47 | code: -1,
48 | message: e && e.toString && e.toString(),
49 | data: null
50 | }
51 | }
52 | })
53 | // bodyParser
54 | app.use(
55 | bodyParser({
56 | multipart: true,
57 | formidable: { maxFields: 5000 },
58 | formLimit: '5mb',
59 | jsonLimit: '5mb'
60 | })
61 | )
62 | // 入参扩展
63 | app.use(extendForCtx({ eventType: entryOptions.scfConfig.testModel }))
64 |
65 | // timeout
66 | app.use(async (ctx, next) => {
67 | return Promise.race([
68 | new Promise((resolve, reject) => {
69 | setTimeout(() => {
70 | reject('timeout')
71 | }, entryOptions.scfConfig.timeout * 1000 + 500)
72 | }),
73 | await next()
74 | ])
75 | })
76 | app.use(
77 | bootstrap(entryOptions.scfConfig.entry, entryOptions.scfConfig.handler,entryOptions.scfConfig.timeout)
78 | )
79 |
80 | return app.listen(entryOptions.port, cb || utils.empty.function)
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/lib/debug/helper/utils.js:
--------------------------------------------------------------------------------
1 | const config = require('../config/config.development')
2 | const util = require('util')
3 |
4 | const mixin = {
5 | isType(type) {
6 | const typeMap = [
7 | 'Number',
8 | 'String',
9 | 'Function',
10 | 'Null',
11 | 'Undefined',
12 | 'Array',
13 | 'Object',
14 | 'Symbol'
15 | ]
16 | if (typeMap.indexOf(type) === -1) throw `type must in ${typeMap}`
17 | return obj => toString.call(obj) == `[object ${type}]`
18 | }
19 | }
20 |
21 | // 生成
22 | module.exports = {
23 | // uuidV4生成requestID
24 | guid() {
25 | function s4() {
26 | return Math.floor((1 + Math.random()) * 0x10000)
27 | .toString(16)
28 | .substring(1)
29 | }
30 | return (
31 | s4() +
32 | s4() +
33 | '-' +
34 | s4() +
35 | '-' +
36 | s4() +
37 | '-' +
38 | s4() +
39 | '-' +
40 | s4() +
41 | s4() +
42 | s4()
43 | )
44 | },
45 | /**
46 | * 克隆一个普通对象,不适合复杂对象
47 | * @param {*} obj
48 | */
49 | clone(obj) {
50 | try {
51 | return JSON.parse(
52 | JSON.stringify(a, (key, value) => {
53 | if (typeof value === 'function') {
54 | return value.toString()
55 | }
56 | return value
57 | })
58 | )
59 | } catch (e) {
60 | return a
61 | }
62 | },
63 | /**
64 | * 值判断,为undefined|null则返回false,其余返回true
65 | * @param {*} str
66 | * @return {boolean}
67 | */
68 | hasValue(str) {
69 | return str ? true : str === undefined || str === null ? false : true
70 | },
71 | /**
72 | * 根据类型整理入参
73 | * @param {*} eventType
74 | */
75 | generateEvent(ctx, eventType) {
76 | if (eventType === 'http') return this.clone(ctx)
77 | const res = config.mock.testModel[eventType]
78 | if (!res)
79 | throw ReferenceError(
80 | '该事件测试模版没有进行过预定义,请先编写测试事件模版'
81 | )
82 | return res
83 | },
84 | empty: {
85 | function: () => {}
86 | },
87 | /**
88 | * 复制一个对象的所有属性,返回新对象
89 | * @param {*} sourceObj
90 | */
91 | clone(sourceObj) {
92 | let destObj = {}
93 | for (let prop in sourceObj) {
94 | destObj[prop] = sourceObj[prop]
95 | }
96 | return destObj
97 | },
98 |
99 | isObject: mixin.isType('Object'),
100 | isArray: mixin.isType('Array'),
101 | isFunction: mixin.isType('Function'),
102 | isNull: mixin.isType('Null'),
103 | isNumber: mixin.isType('Number'),
104 | isString: mixin.isType('String')
105 | }
106 |
--------------------------------------------------------------------------------
/lib/debug/config/config.development.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | host: 'localhost',
3 | port: 3000, // 启动端口
4 | debug: true, // 命令行是否为后台运行
5 | scfConfig: {
6 | // 云函数配置
7 | entry: './lib/debug/demo.js', // 入口文件
8 | handler: 'main_handler', // 执行方法
9 | timeout: 3, // 超时时间
10 | memorySize: 256, // 执行内存
11 | testModel: 'http' // http|apigateway|helloworld|cmq|ckafka
12 | },
13 | mock: {
14 | // 建议跟控制台保持一致
15 | testModel: {
16 | http: null, // 默认为ctx
17 | apigateway: {
18 | requestContext: {
19 | serviceName: 'testsvc',
20 | path: '/test/{path}',
21 | httpMethod: 'POST',
22 | requestId: 'c6af9ac6-7b61-11e6-9a41-93e8deadbeef',
23 | identity: {
24 | secretId: 'abdcdxxxxxxxsdfs'
25 | },
26 | sourceIp: '10.0.2.14',
27 | stage: 'prod'
28 | },
29 | headers: {
30 | 'Accept-Language': 'en-US,en,cn',
31 | Accept: 'text/html,application/xml,application/json',
32 | Host:
33 | 'service-3ei3tii4-251000691.ap-guangzhou.apigateway.myqloud.com',
34 | 'User-Agent': 'User Agent String'
35 | },
36 | body: '{"test":"body"}',
37 | pathParameters: {
38 | path: 'value'
39 | },
40 | queryStringParameters: {
41 | foo: 'bar'
42 | },
43 | headerParameters: {
44 | Refer: '10.0.2.14'
45 | },
46 | stageVariables: {
47 | stage: 'test'
48 | },
49 | path: '/test/value?foo=bar',
50 | httpMethod: 'POST'
51 | },
52 | helloworld: {
53 | key1: 'test value 1',
54 | key2: 'test value 2'
55 | },
56 | cmq: {
57 | Records: [
58 | {
59 | CMQ: {
60 | type: 'topic',
61 | topicOwner: '120xxxxx',
62 | topicName: 'testtopic',
63 | subscriptionName: 'xxxxxx',
64 | publishTime: '1970-01-01T00:00:00.000Z',
65 | msgId: '123345346',
66 | requestId: '123345346',
67 | msgBody: 'Hello from CMQ!',
68 | msgTag: ['tag1', 'tag2']
69 | }
70 | }
71 | ]
72 | },
73 | ckafka: {
74 | Records: [
75 | {
76 | Ckafka: {
77 | topic: 'test-topic', // 消息topic
78 | partition: '', // 来源partition
79 | offset: 123456, // 本消息offset
80 | msgKey: 'asdfwasdfw', // 本消息key, 可选,如无key可以无此项或为None
81 | msgBody: 'Hello from CMQ!' // 消息内容
82 | }
83 | }
84 | ]
85 | }
86 | }
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/test/cli.test.js:
--------------------------------------------------------------------------------
1 | /**************************************************
2 | strin/strout
3 | ***************************************************/
4 | // external depend
5 | const path = require('path')
6 | const EOL = require('os').EOL
7 | const CliTest = require('command-line-test')
8 | const assert = require('assert')
9 | const bddStdin = require('bdd-stdin')
10 | const inquirer = require('inquirer')
11 |
12 | // internal depend
13 | const binFile = require.resolve('../bin')
14 | const pkg = require('../package')
15 | const cliMock = require('./mock/cli.mock')
16 | const { getEntityByObject, validate } = require('./helper/utils')
17 |
18 | describe('scf-cli command-line test', function() {
19 | it('avoid recursive', function() {})
20 | // --help -h
21 | it('`scf -h` should be ok', function() {
22 | var cliTest = new CliTest()
23 | return cliTest.execFile(binFile, ['-h'], {}).then(res => {
24 | var lines = res.stdout.trim().split(EOL)
25 | lines[2].trim().should.be.equal(pkg.description)
26 | })
27 | })
28 |
29 | // --version -V
30 | it('`scf -V` should be ok', function() {
31 | var cliTest = new CliTest()
32 | return cliTest.execFile(binFile, ['-V'], {}).then(res => {
33 | res.stdout.should.containEql(pkg.version)
34 | })
35 | })
36 |
37 | // empty argv
38 | it('`scf` should be ok', function() {
39 | var cliTest = new CliTest()
40 | return cliTest.execFile(binFile, [], {}).then(res => {
41 | var lines = res.stdout.trim().split(EOL)
42 | lines[2].trim().should.be.equal(pkg.description)
43 | })
44 | })
45 |
46 | // wrong argv
47 | it('`scf wrong` should be ok', function() {
48 | var cliTest = new CliTest()
49 | return cliTest.execFile(binFile, ['wrong'], {}).then(res => {
50 | var lines = res.stdout.trim().split(EOL)
51 | lines[2].trim().should.be.equal(pkg.description)
52 | })
53 | })
54 |
55 | // init - 交互式命令行测试
56 | it('`scf init` should be ok', function(done) {
57 | const cliTest = new CliTest()
58 | let prompts = cliMock.promps
59 |
60 | // 模拟输入入口地址、入口方法、超时时间、测试模版
61 | const defaultData = getEntityByObject(cliMock.data).value('defaultData')
62 | let stdinData = getEntityByObject(cliMock.data).value('stdinData')
63 |
64 | // 正确demo
65 | bddStdin(
66 | stdinData.entry, // 这里测试时不做存在校验
67 | '\n',
68 | stdinData.handler,
69 | '\n',
70 | stdinData.timeout.toString(),
71 | '\n',
72 | stdinData.testModel,
73 | '\n'
74 | )
75 | inquirer.prompt(prompts).then(answers => {
76 | let error = null
77 | const response = answers.choice
78 | const isValid = validate(defaultData, stdinData, answers)
79 | console.assert(isValid)
80 | if (!isValid) error = new Error('校验失败')
81 | done(error)
82 | })
83 | })
84 | })
85 |
--------------------------------------------------------------------------------
/test/lib.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const assert = require('assert')
4 | const request = require('supertest')
5 | const lib = require('../lib/debug/bootstrap')
6 | const config = require('../lib/debug/config/config.development')
7 | const logger = require('../lib/debug/lib/logger')
8 |
9 | logger.setLevel(-1)
10 | logger.setPrefix('[TEST]')
11 |
12 | describe('scf lib', () => {
13 | const correctDemo = lib.init({
14 | port: 8082,
15 | scfConfig: {
16 | entry: 'test/mock/correctDemo.js',
17 | handler: 'main_handler',
18 | timeout: config.scfConfig.timeout,
19 | testModel: 'http'
20 | }
21 | })
22 | const errorDemo = lib.init({
23 | port: 8083,
24 | scfConfig: {
25 | entry: 'test/mock/errorDemo.js',
26 | handler: 'main_handler',
27 | timeout: config.scfConfig.timeout,
28 | testModel: 'cmq'
29 | }
30 | })
31 | const timeoutDemo = lib.init({
32 | port: 8084,
33 | scfConfig: {
34 | entry: 'test/mock/timeoutDemo.js',
35 | handler: 'main_handler',
36 | timeout: config.scfConfig.timeout,
37 | testModel: 'cmq'
38 | }
39 | })
40 | const asyncCorrectDemo = lib.init({
41 | port: 8085,
42 | scfConfig: {
43 | entry: 'test/mock/asyncCorrectDemo.js',
44 | handler: 'main_handler',
45 | timeout: config.scfConfig.timeout,
46 | testModel: 'http'
47 | }
48 | })
49 |
50 | const asyncErrorDemo = lib.init({
51 | port: 8086,
52 | scfConfig: {
53 | entry: 'test/mock/asyncErrorDemo.js',
54 | handler: 'main_handler',
55 | timeout: config.scfConfig.timeout,
56 | testModel: 'http'
57 | }
58 | })
59 |
60 | describe('#test sync correct demo', () => {
61 | it('#test GET /', done => {
62 | let res = request(correctDemo)
63 | .get('/')
64 | .expect(res => {
65 | assert.equal(res.text, 'sync-correct', 'sync correct demo')
66 | })
67 | .end(done)
68 | }).timeout(config.scfConfig.timeout * 1000)
69 | })
70 |
71 | describe('#test sync error demo', () => {
72 | it('#test GET /', done => {
73 | let res = request(errorDemo)
74 | .get('/')
75 | .expect(res => {
76 | assert.equal(res.body.data, undefined)
77 | })
78 | .end(done)
79 | }).timeout(config.scfConfig.timeout * 1000)
80 | })
81 |
82 | describe('#test timeout demo', () => {
83 | it('#test GET /', done => {
84 | let res = request(timeoutDemo)
85 | .get('/')
86 | .expect(404)
87 | .end(done)
88 | }).timeout(config.scfConfig.timeout * 1000 * 2)
89 | })
90 |
91 | describe('#test async correct demo', () => {
92 | it('#test GET /', done => {
93 | let res = request(asyncCorrectDemo)
94 | .get('/')
95 | .expect(res => {
96 | assert.equal(res.text, 'async-correct')
97 | })
98 | .end(done)
99 | }).timeout(config.scfConfig.timeout * 1000 * 2)
100 | })
101 |
102 | describe('#test async error demo', () => {
103 | it('#test GET /', done => {
104 | let res = request(asyncErrorDemo)
105 | .get('/')
106 | .expect(res => {
107 | assert.equal(res.body.data, undefined)
108 | })
109 | .end(done)
110 | }).timeout(config.scfConfig.timeout * 1000 * 2)
111 | })
112 |
113 | after(done => {
114 | done()
115 | process.exit(0)
116 | })
117 | })
118 |
--------------------------------------------------------------------------------
/bin/index.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | // external depend
4 | const program = require('commander')
5 | const inquirer = require('inquirer')
6 | const chalk = require('chalk')
7 | const _ = require('lodash')
8 | const path = require('path')
9 |
10 | // internal depend
11 | const pkgJson = require('../package.json')
12 | const config = require('../lib/debug/config/config.development')
13 | const logo = require('../lib/debug/widget/slogan')
14 | const testCli = require('../lib/debug/bootstrap')
15 | const logger = require('../lib/debug/lib/logger')
16 |
17 | // scf配置
18 | const DEFAULT_SCF_CONFIG = config.scfConfig
19 | // 全局配置
20 | const DEFAULT_GLOBAL_CONFIG = {
21 | host: config.host,
22 | port: config.port,
23 | mock: config.mock
24 | }
25 | const testModelMap = Object.keys(DEFAULT_GLOBAL_CONFIG.mock.testModel)
26 |
27 | // 辅助信息
28 | program
29 | .version(pkgJson.version)
30 | .description(`${pkgJson.description}\n${logo}`)
31 | .usage('init|i [options]')
32 |
33 | // 初始化调试工具
34 | program
35 | .command('init [host][port][debug]')
36 | .alias('i')
37 | .description('Init The TestCli For Your App')
38 | .option('-h, --host [host]', 'Input the host To Start The Mock Server')
39 | .option('-p, --port [port]', 'Input the port To Start The Mock Server')
40 | .option('-d, --debug [debug]', 'Start Supervisor')
41 | .action(function(env, options) {
42 | /**************************************************
43 | start of interactive
44 | ***************************************************/
45 | let promps = []
46 | // entry
47 | promps.push({
48 | type: 'input',
49 | name: 'entry',
50 | message: '请输入入口文件地址(相对路径)',
51 | validate: function(input) {
52 | const scriptPath = require.resolve(path.resolve(process.cwd(), input))
53 | if (!input) {
54 | return '请输入入口文件地址'
55 | }
56 | return true
57 | }
58 | })
59 | // handler
60 | promps.push({
61 | type: 'input',
62 | name: 'handler',
63 | default: DEFAULT_SCF_CONFIG.handler,
64 | message: '请输入入口执行方法名称',
65 | validate: function(input) {
66 | return true
67 | }
68 | })
69 | // timeout
70 | promps.push({
71 | type: 'input',
72 | name: 'timeout',
73 | default: DEFAULT_SCF_CONFIG.timeout,
74 | message: '请输入超时时间限制(单位:s)',
75 | validate: function(input) {
76 | if (+input <= 30 && +input >= 1) {
77 | return true
78 | }
79 | return '请输入1-30s的数字'
80 | }
81 | })
82 | // testModel
83 | promps.push({
84 | type: 'list',
85 | name: 'testModel',
86 | choices: testModelMap.map((item, index) => {
87 | return {
88 | name: item,
89 | value: item,
90 | checked: index === 0
91 | }
92 | }),
93 | message: '请选择测试模版',
94 | validate: function(input) {
95 | return true
96 | }
97 | })
98 | /**************************************************
99 | end of interactive
100 | ***************************************************/
101 |
102 | inquirer.prompt(promps).then(answers => {
103 | const commandConfig = {
104 | host: options.host || DEFAULT_GLOBAL_CONFIG.host,
105 | port: options.port || DEFAULT_GLOBAL_CONFIG.port,
106 | debug: options.debug,
107 | scfConfig: answers
108 | }
109 | testCli.init(commandConfig, data => {
110 | logger.info(
111 | `Server has listened [IP]:${commandConfig.host} [PORT]:${
112 | commandConfig.port
113 | }. http://${commandConfig.host}:${commandConfig.port}`
114 | )
115 | })
116 | })
117 | })
118 |
119 | // no command
120 | if (!process.argv.slice(2).length) {
121 | program.outputHelp(txt => {
122 | return txt
123 | })
124 | }
125 |
126 | // error on unknown commands
127 | program.on('command:*', function() {
128 | console.warn(
129 | 'Invalid command: %s\nSee --help for a list of available commands.',
130 | program.args.join(' ')
131 | )
132 |
133 | program.outputHelp(txt => {
134 | return txt
135 | })
136 | })
137 |
138 | program.parse(process.argv)
139 |
--------------------------------------------------------------------------------
/lib/debug/lib/wrapper.js:
--------------------------------------------------------------------------------
1 | // external depend
2 | const path = require('path')
3 | const EventEmitter = require('events')
4 |
5 | // internal depend
6 | const utils = require('../helper/utils')
7 | const logger = require('./logger')
8 | logger.setLevel(1)
9 | /**************************************************
10 | 环境变量
11 | ***************************************************/
12 | const envList = process.env
13 | const entry = envList.entry // 入口文件
14 | const handler = envList.handler // 入口函数
15 | const event = JSON.parse(envList.event) // 请求主体
16 | // const cb = JSON.parse(envList.cb) // 响应请求
17 | /**************************************************
18 | scf执行状态
19 | ***************************************************/
20 | const entryScript = require(path.resolve(__dirname, entry)) // 入口执行脚本
21 | let returnVal = undefined // 入口脚本返回值
22 | let exitCode = 0 // exit code
23 | let error = null // 错误
24 |
25 | /**************************************************
26 | scf入参context/callback
27 | ***************************************************/
28 | // context对象
29 | const context = {
30 | done: callback,
31 | request_id: utils.guid()
32 | }
33 |
34 | // 结束本次执行并返回内容
35 | function callback(err, data) {
36 | error = err
37 | // const tmpBuffer = new Buffer(1073741824)
38 | const isNumber = utils.isNumber(data)
39 | const isString = utils.isString(data)
40 | returnVal = isNumber || isString ? data : JSON.stringify(data)
41 | logger.debug('child_process callback')
42 | // 这里用emit('exit')、send不奏效
43 | ipcSend({ error, returnVal, exitCode }, () => {
44 | logger.info('---Has send from ipc---')
45 | process.exit(0)
46 | })
47 | }
48 |
49 | // 通过ipc与父进程通信
50 | function ipcSend({ error, returnVal, exitCode }, cb) {
51 | try {
52 | process.send({ error, returnVal, exitCode }, cb)
53 | } catch (e) {
54 | logger.debug(`ipcSend has occured error: ${e}`)
55 | }
56 | }
57 |
58 | /**************************************************
59 | scf状态监听
60 | ***************************************************/
61 | // 同步错误捕捉
62 | try {
63 | returnVal = entryScript[handler](event, context, callback)
64 | logger.debug(`returnVal: ${returnVal}`)
65 |
66 | // 返回object
67 | if (typeof returnVal === 'object') {
68 | // async执行完返回一个object
69 | if (returnVal instanceof Promise) {
70 | returnVal.then(res => {
71 | if (typeof res === 'function') {
72 | returnVal = null
73 | } else {
74 | returnVal = res
75 | }
76 | })
77 | }
78 | }
79 | // 返回function,置于null
80 | if (typeof returnVal === 'function') {
81 | returnVal = null
82 | }
83 | } catch (syncErr) {
84 | // 已知类型的错误
85 | if (syncErr.stack) {
86 | throw syncErr
87 | }
88 | // 未知类型的错误默认Error
89 | else {
90 | throw Error(syncErr)
91 | }
92 | }
93 | // promise错误捕捉
94 | process.on('unhandledRejection', asyncErr => {
95 | logger.debug(`child_process unhandledRejection: ${asyncErr}`)
96 | // 已知类型的错误
97 | if (asyncErr.stack) {
98 | throw asyncErr
99 | }
100 | // 未知类型的错误默认Error
101 | else {
102 | throw Error(asyncErr)
103 | }
104 | })
105 | // 兜底捕捉
106 | process.on('uncaughtException', err => {
107 | logger.debug(`child_process uncaughtException: ${err}`)
108 | // 已知类型的错误
109 | if (err.stack) {
110 | throw err
111 | }
112 | // 未知类型的错误默认Error
113 | else {
114 | throw Error(err)
115 | }
116 | })
117 |
118 | process.on('close', err => {
119 | logger.debug(`child_process close: ${err}`)
120 | })
121 |
122 | process.on('error', err => {
123 | logger.debug(`child_process error: ${err}`)
124 | ipcSend({ error, returnVal, exitCode }, () => {
125 | logger.debug('---Has send from ipc---')
126 | })
127 | })
128 |
129 | process.on('rejectionHandled', err => {
130 | logger.debug(`child_process rejectionHandled: ${err}`)
131 | })
132 |
133 | process.on('warning', err => {
134 | logger.debug(`child_process warning: ${err}`)
135 | })
136 |
137 | // FIX: 这里node在9.7.0版本下有bug,console.log本质也是异步实现,这里会进入死循环
138 | process.on('beforeExit', code => {
139 | // logger.debug(`child_process beforeExit: ${code}`)
140 | })
141 |
142 | process.on('exit', code => {
143 | logger.debug(`child_process exit: ${code}`)
144 | exitCode = code
145 | ipcSend({ error, returnVal, exitCode }, () => {
146 | logger.debug('---Has send from ipc---')
147 | })
148 | })
149 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4 |
5 |
6 | ## [1.0.8](https://github.com/TencentCloud/scf-node-debug/compare/v1.0.7...v1.0.8) (2019-08-29)
7 |
8 |
9 |
10 |
11 | ## [1.0.7](https://github.com/TencentCloud/scf-node-debug/compare/v1.0.6...v1.0.7) (2019-03-07)
12 |
13 |
14 |
15 |
16 | ## [1.0.6](https://github.com/TencentCloud/scf-node-debug/compare/v1.0.5...v1.0.6) (2019-03-07)
17 |
18 |
19 |
20 |
21 | ## [1.0.5](https://github.com/TencentCloud/scf-node-debug/compare/v1.0.4...v1.0.5) (2019-03-07)
22 |
23 |
24 |
25 |
26 | ## [1.0.4](https://github.com/TencentCloud/scf-node-debug/compare/v1.0.3...v1.0.4) (2019-01-03)
27 |
28 |
29 | ### Bug Fixes
30 |
31 | * **bootstrap:** transfer all env from parant_process to child_process ([e05993a](https://github.com/TencentCloud/scf-node-debug/commit/e05993a))
32 |
33 |
34 |
35 |
36 | ## [1.0.3](https://github.com/TencentCloud/scf-node-debug/compare/v1.0.2...v1.0.3) (2019-01-03)
37 |
38 |
39 |
40 |
41 | ## [1.0.2](https://github.com/TencentCloud/scf-node-debug/compare/v1.0.1...v1.0.2) (2019-01-03)
42 |
43 |
44 |
45 |
46 | ## [1.0.1](https://github.com/TencentCloud/scf-node-debug/compare/v1.0.0...v1.0.1) (2018-12-27)
47 |
48 |
49 |
50 |
51 | # [1.0.0](https://github.com/TencentCloud/scf-node-debug/compare/v0.0.19-alpha...v1.0.0) (2018-09-10)
52 |
53 |
54 |
55 |
56 | ## [0.0.19-alpha](https://github.com/TencentCloud/scf-node-debug/compare/v0.0.18-alpha...v0.0.19-alpha) (2018-08-21)
57 |
58 |
59 |
60 |
61 | ## [0.0.18-alpha](https://github.com/TencentCloud/scf-node-debug/compare/v0.0.17-alpha...v0.0.18-alpha) (2018-08-21)
62 |
63 |
64 |
65 |
66 | ## [0.0.17-alpha](https://github.com/TencentCloud/scf-node-debug/compare/v0.0.16-alpha...v0.0.17-alpha) (2018-08-20)
67 |
68 |
69 |
70 |
71 | ## [0.0.16-alpha](https://github.com/TencentCloud/scf-node-debug/compare/v0.0.15-alpha...v0.0.16-alpha) (2018-08-13)
72 |
73 |
74 |
75 |
76 | ## [0.0.15-alpha](https://github.com/TencentCloud/scf-node-debug/compare/v0.0.14-alpha...v0.0.15-alpha) (2018-08-13)
77 |
78 |
79 |
80 |
81 | ## [0.0.14-alpha](https://github.com/TencentCloud/scf-node-debug/compare/v0.0.13-alpha...v0.0.14-alpha) (2018-08-09)
82 |
83 |
84 |
85 |
86 | ## [0.0.13-alpha](https://github.com/TencentCloud/scf-node-debug/compare/v0.0.12-alpha...v0.0.13-alpha) (2018-08-09)
87 |
88 |
89 |
90 |
91 | ## [0.0.12-alpha](https://github.com/TencentCloud/scf-node-debug/compare/v0.0.11-alpha...v0.0.12-alpha) (2018-07-29)
92 |
93 |
94 |
95 |
96 | ## [0.0.11-alpha](https://github.com/TencentCloud/scf-node-debug/compare/v0.0.10-alpha...v0.0.11-alpha) (2018-07-29)
97 |
98 |
99 |
100 |
101 | ## [0.0.10-alpha](https://github.com/TencentCloud/scf-node-debug/compare/v0.0.9-alpha...v0.0.10-alpha) (2018-07-29)
102 |
103 |
104 |
105 |
106 | ## [0.0.9-alpha](https://github.com/TencentCloud/scf-node-debug/compare/v0.0.8-alpha...v0.0.9-alpha) (2018-07-29)
107 |
108 |
109 | ### Features
110 |
111 | * **all:** add standby for koa-body ([2426434](https://github.com/TencentCloud/scf-node-debug/commit/2426434))
112 |
113 |
114 |
115 |
116 | ## [0.0.8-alpha](https://github.com/TencentCloud/scf-node-debug/compare/v0.0.7-alpha...v0.0.8-alpha) (2018-07-23)
117 |
118 |
119 |
120 |
121 | ## [0.0.7-alpha](https://github.com/TencentCloud/scf-node-debug/compare/v0.0.6-alpha...v0.0.7-alpha) (2018-07-23)
122 |
123 |
124 | ### Features
125 |
126 | * **test:** add unit test ([da3624f](https://github.com/TencentCloud/scf-node-debug/commit/da3624f))
127 |
128 |
129 |
130 |
131 | ## [0.0.6-alpha](https://github.com/TencentCloud/scf-node-debug/compare/v0.0.5...v0.0.6-alpha) (2018-07-11)
132 |
133 |
134 |
135 |
136 | ## [0.0.5](https://github.com/TencentCloud/scf-node-debug/compare/v0.0.5-alpha...v0.0.5) (2018-07-11)
137 |
138 |
139 |
140 |
141 | ## [0.0.5-alpha](https://github.com/TencentCloud/scf-node-debug/compare/v0.0.4-alpha...v0.0.5-alpha) (2018-07-04)
142 |
--------------------------------------------------------------------------------
/lib/debug/middlewares/bootstrap.js:
--------------------------------------------------------------------------------
1 | // external depend
2 | const path = require('path')
3 | const { fork, spawn } = require('child_process')
4 | const ora = require('ora')
5 | const Table = require('tty-table')
6 | const colors = require('colors/safe')
7 |
8 | // internal depend
9 | const { scfConfig } = require('../config')
10 | const logger = require('../lib/logger')
11 | const utils = require('../helper/utils')
12 | logger.setLevel(3)
13 |
14 | let childProcess
15 |
16 | function kill(process) {
17 | return process.kill()
18 | }
19 |
20 | module.exports = function(entry, handler, timeout) {
21 | return async function(ctx, next) {
22 | if (ctx.request.url === '/favicon.ico') {
23 | return next()
24 | }
25 | /**************************************************
26 | 子进程状态
27 | ***************************************************/
28 | let isDone = false // 子进程是否已经完成;同异步均完成/抛出错误/调用callback/return
29 | let error = null // 捕捉错误
30 | let returnVal = undefined // 入口脚本返回值
31 | let exitCode = 0 // exit code
32 | let MAX_BYTES = 6 * 1024 * 1024 // 云函数日志字节上限
33 | const logHeader = [
34 | {
35 | value: 'Time',
36 | headerColor: 'green',
37 | color: 'white',
38 | align: 'left',
39 | paddingLeft: 1,
40 | width: 150
41 | },
42 | {
43 | value: 'LogText',
44 | headerColor: 'green',
45 | color: 'white',
46 | align: 'left',
47 | paddingLeft: 1
48 | // width: 100
49 | }
50 | ]
51 | const logRows = []
52 | const logOptions = {
53 | borderStyle: 1,
54 | paddingBottom: 0,
55 | headerAlign: 'center',
56 | align: 'center',
57 | color: 'white'
58 | }
59 | let logTable
60 | /**************************************************
61 | 启动子进程
62 | ***************************************************/
63 |
64 | let childTaskEnv = {
65 | entry: path.resolve(process.cwd(), entry),
66 | handler,
67 | event: JSON.stringify(ctx.testModel)
68 | }
69 |
70 | try {
71 | childProcess = fork(path.join(__dirname, '../lib/wrapper'), {
72 | silent: true,
73 | env: Object.assign({}, process.env, childTaskEnv)
74 | })
75 | } catch (e) {
76 | logger.error(e)
77 | }
78 |
79 | /**************************************************
80 | 监听子进程
81 | ***************************************************/
82 | // 接收到信息就代表进程结束
83 | childProcess.on('message', data => {
84 | isDone = true
85 | returnVal = data.returnVal
86 | error = data.error
87 | exitCode = data.exitCode
88 | })
89 | // 接收子进程的console,这里只捕捉云函数内的日志
90 | // 单条日志(console)不超过6M,超过部分会被截断
91 | // 总大小不超过6M,超过部分会被截断
92 | childProcess.stdout.on('data', data => {
93 | if (data.length > MAX_BYTES)
94 | logger.warn(`请注意日志输出长度不要大于${MAX_BYTES}M,超出部分将被丢弃`)
95 | if (logRows.length < MAX_BYTES) {
96 | logRows.push([
97 | logger.now(),
98 | data.slice(0, MAX_BYTES - logRows.length).toString()
99 | ])
100 | } else {
101 | logger.warn(
102 | `收集到的日志总量已经达到${MAX_BYTES}M,此后产生的日志将被丢弃`
103 | )
104 | }
105 | // logger.info(`stdout: ${data}`)
106 | })
107 | // 捕捉子进程的syntaxError
108 | childProcess.stderr.on('data', err => {
109 | logger.error(`运行错误: ${err}`)
110 | isDone = true
111 | error = err.toString()
112 | })
113 | childProcess.on('beforeExit', code => {
114 | // console.log('beforeExit: ', code)
115 | })
116 | // 如果子进程因为出错退出,则提前结束
117 | childProcess.on('exit', code => {
118 | isDone = true
119 | exitCode = code
120 | })
121 | childProcess.on('rejectionHandled', err => {
122 | // console.log('child rejectionHandled', err)
123 | })
124 | childProcess.on('unhandledRejection', err => {
125 | // console.log('child unhandledRejection', err)
126 | })
127 | childProcess.on('warning', err => {
128 | // console.log('child warning', err)
129 | })
130 | childProcess.on('error', err => {
131 | // console.log('child error', err)
132 | })
133 | childProcess.on('close', code => {
134 | // console.log('child close', code)
135 | })
136 |
137 | /**************************************************
138 | 处理返回结果
139 | ***************************************************/
140 | // 轮询获取子进程状态,超时/完成则kill子进程
141 | await new Promise((resolve, reject) => {
142 | let num = 0
143 | let isTimeout = false
144 | let interval = 100
145 | let printReturnVal
146 | const isObject = utils.isObject
147 | const isArray = utils.isArray
148 |
149 | const spinner = ora({
150 | text: logger.debug('SCF运行状态: pending... \n', true)
151 | }).start()
152 |
153 | let timer = setInterval(async () => {
154 | isTimeout = num > (timeout * 1000) / interval
155 |
156 | // 超时
157 | if (isTimeout || isDone) {
158 | clearInterval(timer)
159 | kill(childProcess)
160 |
161 | ctx.body = returnVal
162 |
163 | if (isTimeout) {
164 | spinner.fail(logger.error('SCF运行状态: rejected', true))
165 | }
166 | if (isDone) {
167 | spinner[error ? 'fail' : 'succeed'](
168 | logger[error ? 'error' : 'debug'](
169 | `SCF运行状态: ${error ? 'rejected' : 'resolved'}`,
170 | true
171 | )
172 | )
173 | }
174 |
175 | // 出错捕捉
176 | if (error) {
177 | ctx.body = error
178 | }
179 |
180 | // 超时控制
181 | if (isTimeout) logger.warn('SCF运行超时')
182 | if (!isTimeout) logger.debug('SCF运行结束')
183 | // 对象/数组做序列化,优化展示
184 | if (utils.isObject(returnVal) || utils.isArray(returnVal)) {
185 | printReturnVal = JSON.stringify(returnVal)
186 | } else if (utils.isFunction(returnVal)) {
187 | printReturnVal = returnVal.toString()
188 | } else {
189 | printReturnVal = returnVal
190 | }
191 |
192 | const hasLog = logRows.length !== 0
193 | logger.debug(`运行错误:${error}`)
194 | logger.debug(`运行结果:${printReturnVal}`)
195 | logger.debug(`进程返回码:${exitCode}`)
196 | logger.debug(`日志内容:${hasLog ? '' : '没有日志输出'}`)
197 | if (hasLog) {
198 | logTable = Table(logHeader, logRows, logOptions)
199 | console.log(`${logTable.render()}`)
200 | }
201 |
202 | resolve()
203 | next()
204 | }
205 | num++
206 | }, interval)
207 | })
208 | }
209 | }
210 |
--------------------------------------------------------------------------------