├── 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 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) 4 | [![NPM Version](https://img.shields.io/npm/v/scf-cli.svg?style=flat)](https://www.npmjs.com/package/scf-cli) 5 | [![NODE Version](https://img.shields.io/node/v/scf-cli.svg)](https://www.npmjs.com/package/scf-cli) 6 | [![Travis CI](https://travis-ci.org/TencentCloud/scf-node-debug.svg?branch=master)](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 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) 4 | [![NPM Version](https://img.shields.io/npm/v/scf-cli.svg?style=flat)](https://www.npmjs.com/package/scf-cli) 5 | [![NODE Version](https://img.shields.io/node/v/scf-cli.svg)](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 | --------------------------------------------------------------------------------