commit 4a70313034ee1f3ac5f163caf418cea6138a1b4d Author: Raylin51 Date: Wed Dec 4 18:13:03 2019 +0800 Initial commit diff --git a/.env b/.env new file mode 100644 index 0000000..b54d79e --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +PUBLIC_URL=./ +EXTENSION_ENV=development \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..36fcfce --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +build +node_modules/ +.vscode-test/ +*.vsix +dist +.DS_Store \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..0a18b9c --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "ms-vscode.vscode-typescript-tslint-plugin" + ] +} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..a80592a --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,66 @@ +// A launch configuration that compiles the extension and then opens it inside a new window +// Use IntelliSense to learn about possible attributes. +// Hover to view descriptions of existing attributes. +// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Extension", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}" + ], + "outFiles": [ + "${workspaceFolder}/build/**/*.js" + ], + "preLaunchTask": "npm: webpack-dev" + }, + { + "name": "Server", + "type": "node", + "request": "launch", + "cwd": "${workspaceFolder}", + "program": "${workspaceFolder}/src/debugadapter.ts", + "args": [ + "--server=4711" + ], + "outFiles": [ + "${workspaceFolder}/build/**/*.js" + ] + }, + { + "name": "Views", + "type": "node", + "request": "attach", + "preLaunchTask": "npm: watch", + "stopOnEntry": false + }, + { + "name": "Extension Tests", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}", + "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" + ], + "outFiles": [ + "${workspaceFolder}/build/test/**/*.js" + ], + "preLaunchTask": "npm: watch" + } + ], + "compounds": [ + { + "name": "Extension + Server", + "configurations": ["Extension", "Server"] + }, + { + "name": "Extension + Views", + "configurations": ["Extension", "Views"] + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..30bf8c2 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,11 @@ +// Place your settings in this file to overwrite default and user settings. +{ + "files.exclude": { + "out": false // set this to true to hide the "out" folder with the compiled JS files + }, + "search.exclude": { + "out": true // set this to false to include "out" folder in search results + }, + // Turn off tsc task auto detection since we have the necessary tasks as npm scripts + "typescript.tsc.autoDetect": "off" +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..5e5a0bd --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,44 @@ +// See https://go.microsoft.com/fwlink/?LinkId=733558 +// for the documentation about the tasks.json format +{ + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "webpack-dev", + "problemMatcher": { + "owner": "Webpack (Dev, Continuous)", + "severity": "error", + "fileLocation": "absolute", + "source": "webpack-typescript", + "background": { + "activeOnStart": true, + "beginsPattern": "webpack is watching the files…", + "endsPattern": "Time: (\\d+)ms" + }, + "pattern": [ + { + "regexp": "ERROR in ([^\\(]*)\\((\\d+),(\\d+)\\):", + "file": 1, + "line": 2, + "column": 3 + }, + { + "regexp": "([A-Za-z0-9-]+):(.*)", + "message": 2, + "code": 1 + } + ] + }, + + "isBackground": true, + "presentation": { + "reveal": "never" + }, + "group": { + "kind": "build", + "isDefault": true + } + } + ] +} diff --git a/.vscodeignore b/.vscodeignore new file mode 100644 index 0000000..d031e18 --- /dev/null +++ b/.vscodeignore @@ -0,0 +1,29 @@ +**/*.ts +**/tsconfig.json +**/tslint.json +.DS_Store +src/ +view-src/ +.env +config/ +.vscode/ +scripts/ +public/ +.gitignore +node_modules/ +webpack.config.js +!node_modules/serialport/ +!node_modules/@serialport/ +!node_modules/debug +!node_modules/ms +!node_modules/bindings +!node_modules/file-uri-to-path +!node_modules/7zip-bin +!node_modules/7zip-bin-wrapper +!node_modules/source-map-support +!node_modules/split2 +!node_modules/iconv-lite +!node_modules/buffer-from +!node_modules/safer-buffer +!node_modules/inherits +!node_modules/util-deprecate \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..c98ceb5 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# Change Log + +## 0.1.0 + +- Initial preview release diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f31c534 --- /dev/null +++ b/README.md @@ -0,0 +1,156 @@ +# Kendryte Dev Tool for Visual Studio Code + +[English](./README_EN.md) + +- [使用准备](#使用准备) +- [快速开始](#快速开始) +- [项目结构](#项目结构) +- [界面功能介绍](#界面功能介绍) +- [常见问题](#常见问题) + - [Windows](#Windows) + - [MacOS](#MacOS) + - [Linux](#Linux) + +## 使用准备 + +首先安装 [VSCode](https://code.visualstudio.com/)。安装完毕后在 VSCode Extension 中搜索 Kendryte, 即可快速安装本插件。本插件目前仅支持 Kendryte 官方开发板 KD233。 + +### MacOS 环境准备 + +1.安装 Homebrew + +``` bash +/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" +``` + +2.安装所需依赖 + +``` bash +brew install libusb mpfr +``` + +### Linux 环境准备 + +#### 依赖安装 + +Linux 用户在使用之前需要安装 libftdi-dev libhidapi-dev libusb 。 + +``` bash +sudo apt install libftdi-dev libhidapi-dev libusb-dev +``` + +或者 + +``` bash +sudo yum install libftdi hidapi libusb +``` + +#### 配置调试器权限 + +1.下载 [60-openocd.rules](https://mirrors-kendryte.s3.cn-northwest-1.amazonaws.com.cn/60-openocd.rules) 文件并将文件放入 `/etc/udev/rules.d` + +2.重载 udev + + ``` bash + sudo udevadm control --reload + ``` + +3.添加用户到 plugdev 用户组 + + ``` bash + sudo usermod -aG plugdev $USER + ``` + +## 快速开始 + +1.启动插件后,Kendryte 控制台会自动弹出,点击 Examples 切换至示例项目商店。 + +![image](./resources/readme/quick-start/quick-1.jpeg) + +2.选择一个项目下载至本地并打开。 + +![image](./resources/readme/quick-start/quick-2.jpeg) + +3.点击状态栏中的编译并上传将项目通过串口烧写至开发板。 + +![image](./resources/readme/quick-start/quick-3.jpeg) + +4.在开发板上查看效果。 + +## 项目结构 + +``` Bash +. +├── .vscode +├── CMakeLists.txt +├── README.md +├── build +│   ├── CMakeCache.txt +│   ├── CMakeFiles +│   ├── Makefile +│   ├── ai_image +│   ├── camera-standalone-driver +│   ├── cmake_install.cmake +│   ├── compile_commands.json +│   ├── ${Project-name} +│   ├── ${Project-name}.bin +│   ├── lcd-nt35310-standalone-driver +│   ├── standalone-sdk +│   └── w25qxx-standalone-driver +├── config +│   ├── device-manager.json +│   ├── flash-manager.h +│   ├── flash-manager.json +│   ├── fpioa-config.c +│   ├── fpioa-config.h +│   └── ide-hook-main.c +├── detect.kmodel +├── kendryte-package.json +├── kendryte_libraries +│   ├── ai_image +│   ├── camera-standalone-driver +│   ├── lcd-nt35310-standalone-driver +│   ├── standalone-sdk +│   └── w25qxx-standalone-driver +└──  src +   └── main.c +``` + +- .vscode: 该目录中内容为自动生成,包含了调试选项,编译命令以及一系列插件直接使用的配置文件。 +- CMakeLists.txt: 该文件为插件编译时自动生成的 CMakelists 文件 +- build: 该目录中内容为编译产物,其中 ${Project-name} 以及 ${Project-name}.bin 为编译出的最终文件。 +- config: 该目录中包含开发板中的引脚配置,模型地址分配配置,内容可修改。 +- detect.kmodel: Kendryte 专属模型文件。 +- kendryte-package.json: 项目配置文件,包含项目名,source 文件等基本信息,可修改。 +- kendryte_libraries: 该目录为依赖安装目录,所有的依赖都会安装到该目录下,安装后的依赖库可以直接调用,无需再手动配置 include。正常情况下不应该修改该目录中文件。 +- src: 项目源码目录。 + +## 界面功能介绍 + +![image](./resources/readme/full-screen.png) + +![image](./resources/readme/status-bar.png) + +![image](./resources/readme/kendryte-index.png) + +![image](./resources/readme/kendryte-lib.png) + +## 常见问题 + +### Windows + +1. Q: 为什么调试时启动 Openocd 报错 libusb_error_not_supported ? + + A: 请下载[Zadig](https://zadig.akeo.ie/)将 JLink 驱动转为 Libusb。 + +### MacOS + +### Linux + +1. Q: 为什么调试启动 Openocd 报错 libusb_error_access ? + + A: 请根据上文[配置调试器权限](#配置调试器权限)来获取调试器权限并重新接入调试设备。如果问题仍未解决,请在 issue 中联系我们。 + +2. Q: 为什么烧写时需要 sudo 权限密码? + + A: 只有当前用户没有读取串口设备权限时才会出现需要密码,您也可以自行配置串口设备权限组。 diff --git a/README_EN.md b/README_EN.md new file mode 100644 index 0000000..7f3e915 --- /dev/null +++ b/README_EN.md @@ -0,0 +1,150 @@ +# Kendryte Dev Tool for Visual Studio Code + +[中文版](./README.md) + +- [Prepare](#Prepare) +- [Quick Start](#Quick\ Start) +- [Directory Structure](#Directory\ Structure) +- [Features](#Features) +- [Questions](#Questions) + - [Windows](#Windows) + - [MacOS](#MacOS) + - [Linux](#Linux) + +## Prepare + +Install [VSCode](https://code.visualstudio.com/) on your computer. Search `Kendryte` on VSCode Extension Market and install. This development tool only support `Kendryte KD233` board for now. + +### MacOS environment + +1.Install Homebrew + +``` bash +/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" +``` + +2.Install dependencies + +``` bash +brew install libusb mpfr +``` + +### Linux environment + +#### Install dependencies + +``` bash +sudo apt install libftdi-dev libhidapi-dev libusb-dev +``` + +or + +``` bash +sudo yum install libftdi hidapi libusb +``` + +#### Debugger permission + +1.Download [60-openocd.rules](https://mirrors-kendryte.s3.cn-northwest-1.amazonaws.com.cn/60-openocd.rules) and put it on `/etc/udev/rules.d` + +2.Reload `udev` + + ``` bash + sudo udevadm control --reload + ``` + +3.Add user to `plugdev` group + + ``` bash + sudo usermod -aG plugdev $USER + ``` + +## Quick Start + +1.Kendryte controller will open after installed, click `Examples` tag to switch to the examples store. + +![image](./resources/readme/en/quick-start/quick-1.png) + +2.Select an example and download. + +![image](./resources/readme/en/quick-start/quick-2.png) + +3.Click the `build and upload` button for build and upload to board. + +![image](./resources/readme/en/quick-start/quick-3.png) + +4.Check the board + +## Directory Structure + +``` Bash +. +├── .vscode +├── CMakeLists.txt +├── README.md +├── build +│   ├── CMakeCache.txt +│   ├── CMakeFiles +│   ├── Makefile +│   ├── ai_image +│   ├── camera-standalone-driver +│   ├── cmake_install.cmake +│   ├── compile_commands.json +│   ├── ${Project-name} +│   ├── ${Project-name}.bin +│   ├── lcd-nt35310-standalone-driver +│   ├── standalone-sdk +│   └── w25qxx-standalone-driver +├── config +│   ├── device-manager.json +│   ├── flash-manager.h +│   ├── flash-manager.json +│   ├── fpioa-config.c +│   ├── fpioa-config.h +│   └── ide-hook-main.c +├── detect.kmodel +├── kendryte-package.json +├── kendryte_libraries +│   ├── ai_image +│   ├── camera-standalone-driver +│   ├── lcd-nt35310-standalone-driver +│   ├── standalone-sdk +│   └── w25qxx-standalone-driver +└──  src +   └── main.c +``` + +- .vscode: The contents of this directory are automatically generated, include debug option, build commands and extension's config. +- CMakeLists.txt: This file is automatically generated when build. +- build: The contents of this directory are compiled product. The `${Project-name}.bin` and `${Project-name}` file are target file. +- config: The content of this directory include pin definitions and model address assignment. It can be overwrited. +- detect.kmodel: Kendryte model。 +- kendryte-package.json: The config file of project. Include project name, source files and so on. It can be overwrited. +- kendryte_libraries: This directory is dependencies installation directory. All of dependencies will download on this directory. You shouldn't modify the contents of this directory most of the time. +- src: Source files. + +## Features + +![image](./resources/readme/en/full-screen.png) + +![image](./resources/readme/en/status-bar.png) + +## Questions + +### Windows + +1. Q: Openocd report error: libusb_error_not_supported? + + A: Please download [Zadig](https://zadig.akeo.ie/) and switch `JLink` driver to `Libusb`。 + +### MacOS + +### Linux + +1. Q: Openocd report error: libusb_error_access? + + A: Please read [Debugger permission](#Debugger\ permission) to get the debugger permission and plug in device again. If you still have this problem, please contact us on issue. + +2. Q: Why extension request sudo permission on upload? + + A: If current don't have permission to read serialport device, it will request sudo permission. You can also config serialport devices permission by yourself. diff --git a/config.json b/config.json new file mode 100644 index 0000000..0633576 --- /dev/null +++ b/config.json @@ -0,0 +1,7 @@ +{ + "cdn": "http://kendryte-ide.s3-website.cn-northwest-1.amazonaws.com.cn/", + "third_party": "3rd-party/", + "package_version": "versions.json", + "host": "http://mirrors-kendryte.s3.cn-northwest-1.amazonaws.com.cn/", + "packages": "/package" +} \ No newline at end of file diff --git a/config/env.js b/config/env.js new file mode 100644 index 0000000..211711b --- /dev/null +++ b/config/env.js @@ -0,0 +1,93 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const paths = require('./paths'); + +// Make sure that including paths.js after env.js will read .env variables. +delete require.cache[require.resolve('./paths')]; + +const NODE_ENV = process.env.NODE_ENV; +if (!NODE_ENV) { + throw new Error( + 'The NODE_ENV environment variable is required but was not specified.' + ); +} + +// https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use +const dotenvFiles = [ + `${paths.dotenv}.${NODE_ENV}.local`, + `${paths.dotenv}.${NODE_ENV}`, + // Don't include `.env.local` for `test` environment + // since normally you expect tests to produce the same + // results for everyone + NODE_ENV !== 'test' && `${paths.dotenv}.local`, + paths.dotenv, +].filter(Boolean); + +// Load environment variables from .env* files. Suppress warnings using silent +// if this file is missing. dotenv will never modify any environment variables +// that have already been set. Variable expansion is supported in .env files. +// https://github.com/motdotla/dotenv +// https://github.com/motdotla/dotenv-expand +dotenvFiles.forEach(dotenvFile => { + if (fs.existsSync(dotenvFile)) { + require('dotenv-expand')( + require('dotenv').config({ + path: dotenvFile, + }) + ); + } +}); + +// We support resolving modules according to `NODE_PATH`. +// This lets you use absolute paths in imports inside large monorepos: +// https://github.com/facebook/create-react-app/issues/253. +// It works similar to `NODE_PATH` in Node itself: +// https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders +// Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. +// Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims. +// https://github.com/facebook/create-react-app/issues/1023#issuecomment-265344421 +// We also resolve them to make sure all tools using them work consistently. +const appDirectory = fs.realpathSync(process.cwd()); +process.env.NODE_PATH = (process.env.NODE_PATH || '') + .split(path.delimiter) + .filter(folder => folder && !path.isAbsolute(folder)) + .map(folder => path.resolve(appDirectory, folder)) + .join(path.delimiter); + +// Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be +// injected into the application via DefinePlugin in Webpack configuration. +const REACT_APP = /^REACT_APP_/i; + +function getClientEnvironment(publicUrl) { + const raw = Object.keys(process.env) + .filter(key => REACT_APP.test(key)) + .reduce( + (env, key) => { + env[key] = process.env[key]; + return env; + }, + { + // Useful for determining whether we’re running in production mode. + // Most importantly, it switches React into the correct mode. + NODE_ENV: process.env.NODE_ENV || 'development', + // Useful for resolving the correct path to static assets in `public`. + // For example, . + // This should only be used as an escape hatch. Normally you would put + // images into the `src` and `import` them in code to get their paths. + PUBLIC_URL: publicUrl, + } + ); + // Stringify all values so we can feed into Webpack DefinePlugin + const stringified = { + 'process.env': Object.keys(raw).reduce((env, key) => { + env[key] = JSON.stringify(raw[key]); + return env; + }, {}), + }; + + return { raw, stringified }; +} + +module.exports = getClientEnvironment; diff --git a/config/jest/cssTransform.js b/config/jest/cssTransform.js new file mode 100644 index 0000000..8f65114 --- /dev/null +++ b/config/jest/cssTransform.js @@ -0,0 +1,14 @@ +'use strict'; + +// This is a custom Jest transformer turning style imports into empty objects. +// http://facebook.github.io/jest/docs/en/webpack.html + +module.exports = { + process() { + return 'module.exports = {};'; + }, + getCacheKey() { + // The output is always the same. + return 'cssTransform'; + }, +}; diff --git a/config/jest/fileTransform.js b/config/jest/fileTransform.js new file mode 100644 index 0000000..aab6761 --- /dev/null +++ b/config/jest/fileTransform.js @@ -0,0 +1,40 @@ +'use strict'; + +const path = require('path'); +const camelcase = require('camelcase'); + +// This is a custom Jest transformer turning file imports into filenames. +// http://facebook.github.io/jest/docs/en/webpack.html + +module.exports = { + process(src, filename) { + const assetFilename = JSON.stringify(path.basename(filename)); + + if (filename.match(/\.svg$/)) { + // Based on how SVGR generates a component name: + // https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6 + const pascalCaseFilename = camelcase(path.parse(filename).name, { + pascalCase: true, + }); + const componentName = `Svg${pascalCaseFilename}`; + return `const React = require('react'); + module.exports = { + __esModule: true, + default: ${assetFilename}, + ReactComponent: React.forwardRef(function ${componentName}(props, ref) { + return { + $$typeof: Symbol.for('react.element'), + type: 'svg', + ref: ref, + key: null, + props: Object.assign({}, props, { + children: ${assetFilename} + }) + }; + }), + };`; + } + + return `module.exports = ${assetFilename};`; + }, +}; diff --git a/config/modules.js b/config/modules.js new file mode 100644 index 0000000..c84210a --- /dev/null +++ b/config/modules.js @@ -0,0 +1,141 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const paths = require('./paths'); +const chalk = require('react-dev-utils/chalk'); +const resolve = require('resolve'); + +/** + * Get additional module paths based on the baseUrl of a compilerOptions object. + * + * @param {Object} options + */ +function getAdditionalModulePaths(options = {}) { + const baseUrl = options.baseUrl; + + // We need to explicitly check for null and undefined (and not a falsy value) because + // TypeScript treats an empty string as `.`. + if (baseUrl == null) { + // If there's no baseUrl set we respect NODE_PATH + // Note that NODE_PATH is deprecated and will be removed + // in the next major release of create-react-app. + + const nodePath = process.env.NODE_PATH || ''; + return nodePath.split(path.delimiter).filter(Boolean); + } + + const baseUrlResolved = path.resolve(paths.appPath, baseUrl); + + // We don't need to do anything if `baseUrl` is set to `node_modules`. This is + // the default behavior. + if (path.relative(paths.appNodeModules, baseUrlResolved) === '') { + return null; + } + + // Allow the user set the `baseUrl` to `appSrc`. + if (path.relative(paths.appSrc, baseUrlResolved) === '') { + return [paths.appSrc]; + } + + // If the path is equal to the root directory we ignore it here. + // We don't want to allow importing from the root directly as source files are + // not transpiled outside of `src`. We do allow importing them with the + // absolute path (e.g. `src/Components/Button.js`) but we set that up with + // an alias. + if (path.relative(paths.appPath, baseUrlResolved) === '') { + return null; + } + + // Otherwise, throw an error. + throw new Error( + chalk.red.bold( + "Your project's `baseUrl` can only be set to `src` or `node_modules`." + + ' Create React App does not support other values at this time.' + ) + ); +} + +/** + * Get webpack aliases based on the baseUrl of a compilerOptions object. + * + * @param {*} options + */ +function getWebpackAliases(options = {}) { + const baseUrl = options.baseUrl; + + if (!baseUrl) { + return {}; + } + + const baseUrlResolved = path.resolve(paths.appPath, baseUrl); + + if (path.relative(paths.appPath, baseUrlResolved) === '') { + return { + src: paths.appSrc, + }; + } +} + +/** + * Get jest aliases based on the baseUrl of a compilerOptions object. + * + * @param {*} options + */ +function getJestAliases(options = {}) { + const baseUrl = options.baseUrl; + + if (!baseUrl) { + return {}; + } + + const baseUrlResolved = path.resolve(paths.appPath, baseUrl); + + if (path.relative(paths.appPath, baseUrlResolved) === '') { + return { + 'src/(.*)$': '/src/$1', + }; + } +} + +function getModules() { + // Check if TypeScript is setup + const hasTsConfig = fs.existsSync(paths.appTsConfig); + const hasJsConfig = fs.existsSync(paths.appJsConfig); + + if (hasTsConfig && hasJsConfig) { + throw new Error( + 'You have both a tsconfig.json and a jsconfig.json. If you are using TypeScript please remove your jsconfig.json file.' + ); + } + + let config; + + // If there's a tsconfig.json we assume it's a + // TypeScript project and set up the config + // based on tsconfig.json + if (hasTsConfig) { + const ts = require(resolve.sync('typescript', { + basedir: paths.appNodeModules, + })); + config = ts.readConfigFile(paths.appTsConfig, ts.sys.readFile).config; + // Otherwise we'll check if there is jsconfig.json + // for non TS projects. + } else if (hasJsConfig) { + config = require(paths.appJsConfig); + } + + config = config || {}; + const options = config.compilerOptions || {}; + + const additionalModulePaths = getAdditionalModulePaths(options); + + return { + additionalModulePaths: additionalModulePaths, + webpackAliases: getWebpackAliases(options), + jestAliases: getJestAliases(options), + hasTsConfig, + }; +} + +module.exports = getModules(); diff --git a/config/paths.js b/config/paths.js new file mode 100644 index 0000000..de1ac1f --- /dev/null +++ b/config/paths.js @@ -0,0 +1,90 @@ +'use strict'; + +const path = require('path'); +const fs = require('fs'); +const url = require('url'); + +// Make sure any symlinks in the project folder are resolved: +// https://github.com/facebook/create-react-app/issues/637 +const appDirectory = fs.realpathSync(process.cwd()); +const resolveApp = relativePath => path.resolve(appDirectory, relativePath); + +const envPublicUrl = process.env.PUBLIC_URL; + +function ensureSlash(inputPath, needsSlash) { + const hasSlash = inputPath.endsWith('/'); + if (hasSlash && !needsSlash) { + return inputPath.substr(0, inputPath.length - 1); + } else if (!hasSlash && needsSlash) { + return `${inputPath}/`; + } else { + return inputPath; + } +} + +const getPublicUrl = appPackageJson => + envPublicUrl || require(appPackageJson).homepage; + +// We use `PUBLIC_URL` environment variable or "homepage" field to infer +// "public path" at which the app is served. +// Webpack needs to know it to put the right ` + }) + } + + return ` + + + + + + Kendryte + + + + + + + +
+ + ${scriptTags()} + + ` + } + + private _getScripts(): Array { + const manifest = JSON.parse(fs.readFileSync(path.join(this._extensionPath, 'build/react-views', 'asset-manifest.json'), 'utf-8')) + let entryFiles: Array = manifest.entrypoints + entryFiles = entryFiles.filter((file: string) => { + return !/\.css$/.test(file) + }) + return entryFiles.map((file: string) => { + const scriptPathOnDisk = vscode.Uri.file(path.join(this._extensionPath, 'build/react-views', file)) + const scriptUri = scriptPathOnDisk.with({ scheme: 'vscode-resource' }) + return scriptUri + }) + } +} + +function getNonce() { + let text = "" + const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + for (let i = 0; i < 32; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)) + } + return text +} \ No newline at end of file diff --git a/src/common/webviewResponse/example.ts b/src/common/webviewResponse/example.ts new file mode 100644 index 0000000..a0052ee --- /dev/null +++ b/src/common/webviewResponse/example.ts @@ -0,0 +1,41 @@ +import * as vscode from 'vscode' +import { downloadPackage, urlJoin, jszipUnarchive, systemFilter } from '@utils/index' +import { readFileSync } from 'fs' +import { join } from 'path' + +export const installExample = (exampleName: string, extensionPath: string): Promise => { + return new Promise((resolve, reject) => { + vscode.window.showOpenDialog({ + canSelectFiles: false, + canSelectFolders: true, + canSelectMany: false + }) + .then(uri => { + if (!uri) { + reject() + return + } + const path = systemFilter(uri[0].path.replace(/^\//, ''), uri[0].path, uri[0].path) + const host = JSON.parse(readFileSync(join(extensionPath, 'config.json'), 'utf-8')).host + const exampleUrl = urlJoin(host, 'example', `${exampleName}_0.1.0.zip`) + console.log(exampleUrl) + downloadPackage(path, exampleUrl, `${exampleName}.zip`) + .then(() => { + jszipUnarchive(join(path, `${exampleName}.zip`), join(path, exampleName)) + .then(async () => { + if (vscode.workspace.rootPath) { + await vscode.commands.executeCommand('vscode.openFolder', vscode.Uri.file(join(path, exampleName)), true) + } else { + await vscode.commands.executeCommand('vscode.openFolder', vscode.Uri.file(join(path, exampleName)), false) + } + resolve() + }) + .catch(err => reject(`Extract ${exampleName} failed`)) + }) + .catch(err => { + console.log(err) + reject(`Download ${exampleName} failed`) + }) + }) + }) +} \ No newline at end of file diff --git a/src/common/webviewResponse/index.ts b/src/common/webviewResponse/index.ts new file mode 100644 index 0000000..212d517 --- /dev/null +++ b/src/common/webviewResponse/index.ts @@ -0,0 +1,52 @@ +import * as vscode from 'vscode' +import { localDependenciesReader } from '@common/local-dependencies' +import { installExample } from '@common/webviewResponse/example' +import { createNewProject } from '@common/webviewResponse/newProject' + +export const messageHandler = async (msg: any, extensionPath: string) => { + switch(msg.type) { + case 'check': + const dependencies = await localDependenciesReader() + const response = { + type: "response", + data: { + dependencies: Object.keys(dependencies || {}) + } + } + return response + case 'package': + try { + await vscode.commands.executeCommand('extension.addDependency', msg.name) + } catch(err) { + vscode.window.showErrorMessage(err) + return ({ + type: 'error', + error: 'Something wrong' + }) + } + return({}) + case 'example': + try { + await installExample(msg.name, extensionPath) + } catch(err) { + vscode.window.showErrorMessage(err) + return ({ + type: 'error', + error: 'Download example failed.' + }) + } + return({}) + case 'create': + try { + await createNewProject() + } catch(err) { + vscode.window.showErrorMessage(err) + return ({ + type: 'error', + error: 'Create new project failed.' + }) + } + return({}) + default: + } +} \ No newline at end of file diff --git a/src/common/webviewResponse/newProject.ts b/src/common/webviewResponse/newProject.ts new file mode 100644 index 0000000..8afe7a4 --- /dev/null +++ b/src/common/webviewResponse/newProject.ts @@ -0,0 +1,58 @@ +import * as vscode from 'vscode' +import * as fs from 'fs' +import { readdirPromisify } from '@utils/index' +import { join } from 'path' + +const copyDir = (path: string, targetPath: string) => { + return new Promise((resolve, reject) => { + try { + fs.mkdirSync(targetPath) + } catch(e) { + console.log(e) + } + readdirPromisify(path) + .then(items => { + items.map(item => { + const itemPath = join(path, item) + if (fs.statSync(itemPath).isDirectory()) { + copyDir(join(path, item), join(targetPath, item)) + } else { + const buffer = fs.readFileSync(join(path, item)) + try { + fs.writeFileSync(join(targetPath, item), buffer) + } catch (err) { + reject(`Create file ${item} failed`) + console.log(err) + } + } + }) + resolve() + }) + .catch(err => { + console.log(err) + reject('No such directory.') + }) + }) +} + +export const createNewProject = () => { + return new Promise((resolve, reject) => { + vscode.window.showOpenDialog({ + canSelectFiles: false, + canSelectFolders: true, + canSelectMany: false + }) + .then(uri => { + if (!uri || !process.env.packagePath) { + reject() + return + } + copyDir(join(process.env.packagePath, 'hello-world-project'), join(uri[0].path, 'hello-world')) + .then(async () => { + await vscode.commands.executeCommand('vscode.openFolder', vscode.Uri.file(join(uri[0].path, 'hello-world')), true) + resolve() + }) + .catch(err => reject(err)) + }) + }) +} \ No newline at end of file diff --git a/src/debug/backend/gdbDataStructs/stack.ts b/src/debug/backend/gdbDataStructs/stack.ts new file mode 100644 index 0000000..370dd8d --- /dev/null +++ b/src/debug/backend/gdbDataStructs/stack.ts @@ -0,0 +1,5 @@ +import { IGDBStackFrame } from './stackFrame' + +export interface IGDBStack { + frame: IGDBStackFrame +} \ No newline at end of file diff --git a/src/debug/backend/gdbDataStructs/stackFrame.ts b/src/debug/backend/gdbDataStructs/stackFrame.ts new file mode 100644 index 0000000..acaf5eb --- /dev/null +++ b/src/debug/backend/gdbDataStructs/stackFrame.ts @@ -0,0 +1,9 @@ +export interface IGDBStackFrame { + level: string // number + addr: string // hex + func: string + file: string + fullname: string + line?: string // number + from?: string // number ??? +} \ No newline at end of file diff --git a/src/debug/backend/gdbDataStructs/thread.ts b/src/debug/backend/gdbDataStructs/thread.ts new file mode 100644 index 0000000..4d860c4 --- /dev/null +++ b/src/debug/backend/gdbDataStructs/thread.ts @@ -0,0 +1,9 @@ +import { IGDBThreadFrame } from './threadFrame' + +export interface IGDBThread { + id: string + 'target-id': string + name?: string + frame: IGDBThreadFrame + state: string +} diff --git a/src/debug/backend/gdbDataStructs/threadFrame.ts b/src/debug/backend/gdbDataStructs/threadFrame.ts new file mode 100644 index 0000000..92312ea --- /dev/null +++ b/src/debug/backend/gdbDataStructs/threadFrame.ts @@ -0,0 +1,9 @@ +export interface IGDBThreadFrame { + level: string // number + addr: string // hex + func: string + args: string[] + file: string + fullname: string + line: string // number +} diff --git a/src/debug/backend/gdbDataStructs/variable.ts b/src/debug/backend/gdbDataStructs/variable.ts new file mode 100644 index 0000000..3715c0d --- /dev/null +++ b/src/debug/backend/gdbDataStructs/variable.ts @@ -0,0 +1,5 @@ +export interface IGDBVariable { + name: string + type: string + value: string +} \ No newline at end of file diff --git a/src/debug/backend/kendryteDebugger.ts b/src/debug/backend/kendryteDebugger.ts new file mode 100644 index 0000000..c5d4239 --- /dev/null +++ b/src/debug/backend/kendryteDebugger.ts @@ -0,0 +1,628 @@ +import { posix } from 'path' +import { DebugSession, Handles, InitializedEvent, Scope, Source, StackFrame, Thread } from 'vscode-debugadapter' +import { DebugProtocol } from 'vscode-debugprotocol' +import { IMyLogger } from '@utils/baseLogger' +import { objectPath } from '../common/library/objectPath' +import { errorMessage, errorStack } from '../common/library/strings' +import { isCommandIssueWhenRunning } from '../common/mi2/mi2AutomaticResponder' +import { createStopEvent, StopReason } from '../common/mi2/pause' +import { BreakpointType, MyBreakpointFunc, MyBreakpointLine } from '../common/mi2/types' +import { toProtocolBreakpoint } from '../common/mi2/types.convert' +import { BackendLogger } from './lib/backendLogger' +import { IDebugConsole, wrapDebugConsole } from './lib/duplexDebugConsole' +import { ErrorCode, ErrorMi2, handleMethodPromise } from './lib/handleMethodPromise' +import { expandValue } from './session/gdb_expansion' +import { DebuggingSession } from './session/session' +import { AttachRequestArguments, LaunchRequestArguments, ValuesFormattingMode, VariableObject } from './type' + +const resolve = posix.resolve +const relative = posix.relative + +class ExtendedVariable { + constructor(public name: VariableId, public options: any) { + } +} + +type VariableId = number | string | VariableObject | ExtendedVariable + +enum ErrorCodeValue { + Unknown = 1, + Restart, + Initialize, + Evaluate, + Attach, + Launch, + Disconnect, + SetVariable, + Breakpoints, + Threads, + StackTrace, + init, + Scopes, + VariableNumber, + VariableString, + ExpandVariable, + VariableObject, + VariableExtend, + Continue, + Pause, + StepIn, + StepOut, + StepNext, +} + +const STACK_HANDLES_START = 1000 +const VAR_HANDLES_START = 512 * 256 + 1000 + +export class KendryteDebugger extends DebugSession { + protected variableHandles = new Handles(VAR_HANDLES_START) + protected variableHandlesReverse: { [id: string]: number } = {} + protected useVarObjects!: boolean + protected debugInstance!: DebuggingSession + + protected readonly debugLogger: IMyLogger + protected readonly vscodeProtocolLogger: IMyLogger + private readonly debugConsole: IDebugConsole + + private initComplete: boolean = false + private autoContinue: boolean = true + private firstThreadRequest: boolean = true + + public constructor(debuggerLinesStartAt1: boolean, isServer: boolean = false) { + super(debuggerLinesStartAt1, isServer) + + this.vscodeProtocolLogger = new BackendLogger('protocol', this) + this.debugLogger = new BackendLogger('gdb', this) + + this.debugConsole = wrapDebugConsole(this, this.vscodeProtocolLogger) + + process.on('unhandledRejection', (reason, p) => { + p.catch((e) => { + if (e instanceof ErrorMi2) { + this.debugLogger.error('Unhandled Mi2 Rejection: ' + (e.node.token ? 'token=' + e.node.token : 'result=' + e.node.rawLine)) + this.debugLogger.error(errorStack(e)) + } else { + this.debugLogger.error('Unhandled Rejection: ' + errorStack(e)) + } + this.debugConsole.errorUser('[kendryte debug] Unhandled Rejection: ' + errorMessage(e)) + }) + }) + + process.on('uncaughtException', (err) => { + console.error(err) + this.debugLogger.error('uncaughtException: ' + errorStack(err)) + this.debugConsole.errorUser('[kendryte debug] Fatal error during debugging session. Catched unhandled exception.\n' + errorStack(err)) + setTimeout(() => { + process.exit(1) + }, 2000) + }) + } + + private fireEvent(ev: DebugProtocol.Event) { + if (this.autoContinue && !this.initComplete && (ev.event === 'stopped' || ev.event === 'continued')) { + this.vscodeProtocolLogger.info('initialize not complete, event muted:', JSON.stringify(ev)) + return + } + this.vscodeProtocolLogger.info('fireEvent: [%s]%s', ev.event, JSON.stringify(ev.body)) + this.sendEvent(ev) + } + + private resetStatus() { + delete this.debugInstance + this.initComplete = false + this.autoContinue = true + } + + private async attachOrLaunch(args: AttachRequestArguments | LaunchRequestArguments, load: boolean) { + if (this.debugInstance) { + await this.debugInstance.terminate() + } + + const logger = new BackendLogger(args.id ? 'gdb-' + args.id : 'gdb-main', this) + const debugConsole = wrapDebugConsole(this, logger) + this.debugInstance = new DebuggingSession(args, logger, debugConsole) + this.debugInstance.onEvent((ev) => this.fireEvent(ev)) + + await this.debugInstance.connect(load) + this.setValuesFormattingMode(args.valuesFormatting) + + this.debugInstance.disconnected.then((self) => { + if (self === this.debugInstance) { + this.resetStatus() + } + }) + + this.sendEvent(new InitializedEvent()) + this.initComplete = true + } + + protected setValuesFormattingMode(mode: ValuesFormattingMode) { + switch (mode) { + case 'disabled': + this.useVarObjects = true + // this.debugInstance.prettyPrint = false + break + case 'prettyPrinters': + this.useVarObjects = true + // this.debugInstance.prettyPrint = true + break + case 'parseText': + default: + this.useVarObjects = false + // this.debugInstance.prettyPrint = false + } + } + + // Supports 256 threads. + protected threadAndLevelToFrameId(threadId: number, level: number) { + return level << 8 | threadId + } + + protected frameIdToThreadAndLevel(frameId: number) { + return [frameId & 0xff, frameId >> 8] + } + + private _createVariable(arg: VariableId, options?: any) { + if (options) { + return this.variableHandles.create(new ExtendedVariable(arg, options)) + } else { + return this.variableHandles.create(arg) + } + } + + private _findOrCreateVariable(varObj: VariableObject): number { + let id: number + if (this.variableHandlesReverse.hasOwnProperty(varObj.name)) { + id = this.variableHandlesReverse[varObj.name] + } else { + id = this._createVariable(varObj) + this.variableHandlesReverse[varObj.name] = id + } + return varObj.isCompound() ? id : 0 + } + + @handleMethodPromise(ErrorCodeValue.VariableNumber) + private async variableNumber(response: DebugProtocol.VariablesResponse, id: number): Promise { + const variables: DebugProtocol.Variable[] = [] + const [threadId, level] = this.frameIdToThreadAndLevel(id) + const stack = await this.debugInstance.getStackVariables(threadId, level) + for (const variable of stack) { + if (this.useVarObjects) { + try { + const varObjName = `var_${id}_${variable.name}` + let varObj: VariableObject + try { + const changes = await this.debugInstance.varUpdate(varObjName) + const changelist = changes.result('changelist') + changelist.forEach((change: any) => { + const name = objectPath(change, 'name') + const vId = this.variableHandlesReverse[name] + const v = this.variableHandles.get(vId) as any + v.applyChanges(change) + }) + const varId = this.variableHandlesReverse[varObjName] + varObj = this.variableHandles.get(varId) as any + } catch (err) { + if (err.message.includes('Variable object not found')) { + varObj = await this.debugInstance.varCreate(variable.name, varObjName) + const varId = this._findOrCreateVariable(varObj) + varObj.exp = variable.name + varObj.id = varId + } else { + return Promise.reject(err) + } + } + variables.push(varObj.toProtocolVariable()) + } catch (err) { + variables.push({ + name: variable.name, + value: `<${err}>`, + variablesReference: 0, + }) + } + } else { + if (variable.valueStr !== undefined) { + let expanded = expandValue(this._createVariable.bind(this), `{${variable.name}=${variable.valueStr})`, '', variable.raw) + if (expanded) { + if (typeof expanded[0] == 'string') { + expanded = [ + { + name: '', + value: prettyStringArray(expanded), + variablesReference: 0, + }, + ] + } + variables.push(expanded[0]) + } + } else { + variables.push({ + name: variable.name, + type: variable.type, + value: '', + variablesReference: this._createVariable(variable.name), + }) + } + } + } + response.body = { variables } + } + + @handleMethodPromise(ErrorCodeValue.VariableString) + private async variableString(response: DebugProtocol.VariablesResponse, id: string) { + // Variable members + // TODO: this evals on an (effectively) unknown thread for multithreaded programs. + const variable = await this.debugInstance.evalExpression(JSON.stringify(id), 0, 0) + let expanded = expandValue(this._createVariable.bind(this), variable.result('value'), id, variable) + if (!expanded) { + throw new ErrorCode(ErrorCodeValue.ExpandVariable, `Could not expand variable`) + } else { + if (typeof expanded[0] == 'string') { + expanded = [ + { + name: '', + value: prettyStringArray(expanded), + variablesReference: 0, + }, + ] + } + response.body = { variables: expanded } + } + } + + @handleMethodPromise(ErrorCodeValue.VariableObject) + private async variableObject(response: DebugProtocol.VariablesResponse, id: VariableObject) { + // Variable members + const children: VariableObject[] = await this.debugInstance.varListChildren(id.name) + const vars = children.map(child => { + child.id = this._findOrCreateVariable(child) + return child.toProtocolVariable() + }) + + response.body = { variables: vars } + } + + @handleMethodPromise(ErrorCodeValue.VariableExtend) + private async variableExtended(response: DebugProtocol.VariablesResponse, varReq: ExtendedVariable) { + response.body = { variables: [] } + + if (varReq.options.arg) { + let argsPart = true + let arrIndex = 0 + const addOne = async () => { + // TODO: this evals on an (effectively) unknown thread for multithreaded programs. + const variable = await this.debugInstance.evalExpression(JSON.stringify(`${varReq.name}+${arrIndex})`), 0, 0) + const expanded = expandValue(this._createVariable.bind(this), variable.result('value'), '' + varReq.name, variable) + if (!expanded) { + throw new ErrorCode(ErrorCodeValue.ExpandVariable, `Could not expand variable`) + } + try { + if (typeof expanded == 'string') { + if (expanded == '') { + if (argsPart) { + argsPart = false + } else { + return + } + } else if (expanded[0] != '"') { + response.body.variables.push({ + name: '[err]', + value: expanded, + variablesReference: 0, + }) + return + } + response.body.variables.push({ + name: `[${(arrIndex++)}]`, + value: expanded, + variablesReference: 0, + }) + await addOne() + } else { + response.body.variables.push({ + name: '[err]', + value: expanded, + variablesReference: 0, + }) + return + } + } catch (e) { + throw new ErrorCode(ErrorCodeValue.ExpandVariable, `Could not expand variable: ${e}`) + } + } + await addOne() + } else { + throw new ErrorCode(13, `Unimplemented variable request options: ${JSON.stringify(varReq.options)}`) + } + } + + @handleMethodPromise(ErrorCodeValue.Initialize) + protected initializeRequest(response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments): void { + this.vscodeProtocolLogger.info('initializeRequest: ') + if (response.body) { + response.body.supportsConfigurationDoneRequest = true + response.body.supportsConditionalBreakpoints = true + response.body.supportsFunctionBreakpoints = true + response.body.supportsEvaluateForHovers = true + response.body.supportsSetVariable = true + response.body.supportsRestartRequest = true + response.body.supportsLogPoints = true + } + } + + @handleMethodPromise(ErrorCodeValue.Disconnect) + disconnectRequest(response: DebugProtocol.DisconnectResponse, args: DebugProtocol.DisconnectArguments) { + return this.debugInstance.terminate() + } + + @handleMethodPromise(ErrorCodeValue.Launch) + protected launchRequest(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments) { + return this.attachOrLaunch(args, true) + } + + @handleMethodPromise(ErrorCodeValue.Attach) + protected async attachRequest(response: DebugProtocol.AttachResponse, args: AttachRequestArguments) { + return this.attachOrLaunch(args, false) + } + + @handleMethodPromise(ErrorCodeValue.Restart) + async restartRequest(response: DebugProtocol.RestartResponse, args: DebugProtocol.RestartArguments) { + return this.debugInstance.reload() + } + + @handleMethodPromise(ErrorCodeValue.Breakpoints) + async setBreakPointsRequest(response: DebugProtocol.SetBreakpointsResponse, args: DebugProtocol.SetBreakpointsArguments) { + await this.debugInstance.connected + + const myBreaks: MyBreakpointLine[] = args.breakpoints ? args.breakpoints.map((bk) => { + return { + type: BreakpointType.Line, + file: args.source.path, + line: bk.line, + condition: bk.condition, + logMessage: bk.logMessage, + } + }) : [] + + const resultBreaks = await this.debugInstance.updateBreakPoints(args.source.path || '', myBreaks) + + if (!response.body) { + response.body = { breakpoints: [] } + } + if (!response.body.breakpoints) { + response.body.breakpoints = [] + } + for (const newBreak of resultBreaks) { + response.body.breakpoints.push(toProtocolBreakpoint(newBreak)) + } + } + + @handleMethodPromise(ErrorCodeValue.Breakpoints) + async setFunctionBreakPointsRequest(response: DebugProtocol.SetFunctionBreakpointsResponse, args: DebugProtocol.SetFunctionBreakpointsArguments) { + await this.debugInstance.connected + + const myBreaks: MyBreakpointFunc[] = args.breakpoints.map((bk) => { + return { + type: BreakpointType.Function, + name: bk.name, + condition: bk.condition, + } + }) + const resultBreaks = await this.debugInstance.updateBreakPoints('', myBreaks) + + if (!response.body) { + response.body = { breakpoints: [] } + } + if (!response.body.breakpoints) { + response.body.breakpoints = [] + } + for (const newBreak of resultBreaks) { + response.body.breakpoints.push(toProtocolBreakpoint(newBreak)) + } + } + + @handleMethodPromise(ErrorCodeValue.init) + protected configurationDoneRequest(response: DebugProtocol.ConfigurationDoneResponse, args: DebugProtocol.ConfigurationDoneArguments): void { + this.debugConsole.logUser('Configuration done!') + + setImmediate(() => { + if (this.autoContinue && !this.debugInstance.isRunning) { + this.debugInstance.continue().finally(() => { + this.debugLogger.writeln('') + }) + } else if (!this.autoContinue && this.debugInstance.isRunning) { + this.debugInstance.interrupt().finally(() => { + this.debugLogger.writeln('') + }) + } + }) + } + + @handleMethodPromise(ErrorCodeValue.Continue) + continueRequest(response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments) { + console.log('get continue') + return this.debugInstance.continue() + } + + @handleMethodPromise(ErrorCodeValue.StepNext) + protected nextRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments) { + console.log('get next') + return this.debugInstance.next() + } + + @handleMethodPromise(ErrorCodeValue.StepIn) + stepInRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments) { + return this.debugInstance.step() + } + + @handleMethodPromise(ErrorCodeValue.StepOut) + stepOutRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments) { + return this.debugInstance.stepOut() + } + + @handleMethodPromise(ErrorCodeValue.Pause) + async pauseRequest(response: DebugProtocol.PauseResponse, args: DebugProtocol.PauseArguments) { + await this.debugInstance.interrupt() + setTimeout(() => { + this.fireEvent(createStopEvent(StopReason.Pausing, undefined, 'pause button clicked')) + }, 500) + } + + @handleMethodPromise(ErrorCodeValue.Threads) + async threadsRequest(response: DebugProtocol.ThreadsResponse) { + if (!this.debugInstance) { + return + } + const threads = await this.debugInstance.getThreads(this.firstThreadRequest).catch((e) => { + if (isCommandIssueWhenRunning(e)) { + return [] + } else { + throw e + } + }) + + this.firstThreadRequest = false + + response.body = { + threads: [], + } + for (const thread of threads) { + let threadName = thread.name + // TODO: Thread names are undefined on LLDB + if (threadName === undefined) { + threadName = thread.targetId + } + if (threadName === undefined) { + threadName = '' + } + response.body.threads.push(new Thread(thread.id, thread.id + ':' + threadName)) + } + } + + @handleMethodPromise(ErrorCodeValue.StackTrace) + protected async stackTraceRequest(response: DebugProtocol.StackTraceResponse, args: DebugProtocol.StackTraceArguments) { + const stack = await this.debugInstance.getStack(args.levels || 0, args.threadId) + const ret: StackFrame[] = [] + stack.forEach(element => { + let source = undefined + let file = element.file + if (file) { + if (process.platform === 'win32') { + if (file.startsWith('\\cygdrive\\') || file.startsWith('/cygdrive/')) { + file = file[10] + ':' + file.substr(11) // replaces /cygdrive/c/foo/bar.txt with c:/foo/bar.txt + } + } + source = new Source(element.fileName, file) + } + + ret.push(new StackFrame( + this.threadAndLevelToFrameId(args.threadId, element.level), + element.function + '@' + element.address, + source, + element.line, + 0, + )) + }) + response.body = { + stackFrames: ret, + } + } + + /* + @handleMethodPromise(4, 'Could not step back: %s - Try running \'target record-full\' before stepping back') + protected stepBackRequest(response: DebugProtocol.StepBackResponse, args: DebugProtocol.StepBackArguments): void { + return this.debugInstance.step() + } + */ + + @handleMethodPromise(ErrorCodeValue.Scopes) + protected scopesRequest(response: DebugProtocol.ScopesResponse, args: DebugProtocol.ScopesArguments) { + const scopes: Scope[] = [] + scopes.push(new Scope('Local', STACK_HANDLES_START + (parseInt(args.frameId as any) || 0), false)) + + response.body = { + scopes: scopes, + } + } + + protected variablesRequest(response: DebugProtocol.VariablesResponse, args: DebugProtocol.VariablesArguments): Promise | undefined { + let id: VariableId + if (args.variablesReference < VAR_HANDLES_START) { + id = args.variablesReference - STACK_HANDLES_START + } else { + id = this.variableHandles.get(args.variablesReference) + } + + if (typeof id == 'number') { + return this.variableNumber(response, id) + } else if (typeof id == 'string') { + return this.variableString(response, id) + } else if (id && id instanceof VariableObject) { + return this.variableObject(response, id) + } else if (id && id instanceof ExtendedVariable) { + return this.variableExtended(response, id) + // } else if (typeof id === 'object') { + // response.body = { + // variables: id as any, + // } + // this.sendResponse(response) + } else { + response.body = { variables: [] } + this.sendResponse(response) + } + } + + @handleMethodPromise(ErrorCodeValue.SetVariable) + async setVariableRequest(response: DebugProtocol.SetVariableResponse, args: DebugProtocol.SetVariableArguments): Promise { + if (this.useVarObjects) { + let name = args.name + if (args.variablesReference >= VAR_HANDLES_START) { + const parent = this.variableHandles.get(args.variablesReference) as VariableObject + name = `${parent.name}.${name}` + } + + const newValue = await this.debugInstance.varAssign(name, args.value) + response.body = { value: newValue } + } else { + const value = await this.debugInstance.changeVariable(args.name, args.value) + response.body = { value } + } + } + + @handleMethodPromise(ErrorCodeValue.Evaluate) + async evaluateRequest(response: DebugProtocol.EvaluateResponse, args: DebugProtocol.EvaluateArguments) { + const [threadId, level] = this.frameIdToThreadAndLevel(args.frameId || 0) + if (args.context == 'watch' || args.context == 'hover') { + const res = await this.debugInstance.evalExpression(args.expression, threadId, level) + response.body = { + variablesReference: 0, + result: res.result('value'), + } + } else { + const output = await this.debugInstance.sendUserInput(args.expression, threadId, level) + if (typeof output == 'undefined') { + response.body = { + result: '', + variablesReference: 0, + } + } else { + response.body = { + result: JSON.stringify(output), + variablesReference: 0, + } + } + } + } +} + +function prettyStringArray(strings: any) { + if (typeof strings == 'object') { + if (strings.length !== undefined) { + return strings.join(', ') + } else { + return JSON.stringify(strings) + } + } else { + return strings + } +} diff --git a/src/debug/backend/lib/backendLogger.ts b/src/debug/backend/lib/backendLogger.ts new file mode 100644 index 0000000..e85b75a --- /dev/null +++ b/src/debug/backend/lib/backendLogger.ts @@ -0,0 +1,42 @@ +import { DebugSession, Event } from 'vscode-debugadapter' +import { LogLevel, NodeLoggerCommon } from '@utils/baseLogger' +import { ILogEventBody } from '@debug/common/eventProtocol' + +export class CustomEvent extends Event { + constructor(type: string, event: T) { + super('custom', { + type, + event, + }) + } +} + +export class BackendLogger extends NodeLoggerCommon { + constructor( + tag: string, + private readonly session: DebugSession, + ) { + super(tag) + } + + public clear(): void { + this.session.sendEvent( + new CustomEvent('clear-log', undefined), + ) + } + + public printLine(tag: string, level: LogLevel, message: string) { + if (message === '') { + this.session.sendEvent( + new CustomEvent('nl', {}), + ) + } else { + this.session.sendEvent( + new CustomEvent('log', { + level: level, + message: `${tag}: ${message.replace(/^/g, ' ').trim()}`, + }), + ) + } + } +} diff --git a/src/debug/backend/lib/duplexDebugConsole.ts b/src/debug/backend/lib/duplexDebugConsole.ts new file mode 100644 index 0000000..33e0b97 --- /dev/null +++ b/src/debug/backend/lib/duplexDebugConsole.ts @@ -0,0 +1,26 @@ +import { DebugSession, OutputEvent } from 'vscode-debugadapter' +import { IMyLogger } from '@utils/baseLogger' + +export interface IDebugConsole { + log(message: string): void + error(message: string): void + logUser(message: string): void + errorUser(message: string): void +} + +export function wrapDebugConsole(session: DebugSession, logger: IMyLogger): IDebugConsole { + return { + logUser(message: string) { + this.log(message + '\n') + }, + errorUser(message: string) { + this.error(message + '\n') + }, + log(message: string) { + session.sendEvent(new OutputEvent(message, 'stdout')) + }, + error(message: string) { + session.sendEvent(new OutputEvent(message, 'stderr')) + }, + } +} \ No newline at end of file diff --git a/src/debug/backend/lib/handleMethodPromise.ts b/src/debug/backend/lib/handleMethodPromise.ts new file mode 100644 index 0000000..4c8eb8d --- /dev/null +++ b/src/debug/backend/lib/handleMethodPromise.ts @@ -0,0 +1,65 @@ +import { Response } from 'vscode-debugadapter' +import { dumpJson } from '../../common/library/strings' +import { IResultNode } from '../../common/mi2/mi2Node' + +const responseSymbol = Symbol('alreadyResponse') + +interface ResponseExpand extends Response { + [responseSymbol]: (String | Symbol)[] +} + +export function handleMethodPromise(code?: number, actionTitle?: string): MethodDecorator { + return (target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor) => { + actionTitle = actionTitle || `GDB: cannot ${propertyKey.toString()}: {message}` + if (typeof descriptor.value === 'function') { + const original: Function = descriptor.value + descriptor.value = function (this: any, response: ResponseExpand, ...others: any[]) { + this.vscodeProtocolLogger.info(`::%s()`, propertyKey) + + return Promise.resolve(original.apply(this, arguments)).then(() => { + if (response[responseSymbol]) { + response[responseSymbol].push(propertyKey) + this.vscodeProtocolLogger.error(`::%s() duplicate resolve, stack = %s`, propertyKey, response[responseSymbol]) + } else { + this.vscodeProtocolLogger.trace('result', dumpJson(response)) + + this.vscodeProtocolLogger.info(`::%s() resolved`, propertyKey) + this.vscodeProtocolLogger.writeln('') + response[responseSymbol] = [propertyKey] + this.sendResponse(response) + } + }, (e) => { + if (!(e instanceof Error)) { + const msg = e ? e.message || 'NoMessage' : 'NoMessage' + e = new Error('Unknown reason: ' + msg) + } + + this.debugConsole.error('Internal error occurred. See log for more information.') + this.vscodeProtocolLogger.info(`::%s() rejected:\n~~~~~~~~~~~~~~~~~~~\n%s\n~~~~~~~~~~~~~~~~~~~`, propertyKey, e.stack) + this.vscodeProtocolLogger.writeln('') + code = (e instanceof ErrorCode) ? e.code : 233 + this.sendErrorResponse(response, code, actionTitle, { + message: e.message, + code, + stack: e.stack, + }) + }) + } + return descriptor + } else { + throw new Error('Cannot decorate ' + propertyKey.toString()) + } + } +} + +export class ErrorCode extends Error { + constructor(public readonly code: number, message: string) { + super(message) + } +} + +export class ErrorMi2 extends Error { + constructor(public readonly node: IResultNode, message: string) { + super(message) + } +} diff --git a/src/debug/backend/lib/merge_env.ts b/src/debug/backend/lib/merge_env.ts new file mode 100644 index 0000000..1ea1166 --- /dev/null +++ b/src/debug/backend/lib/merge_env.ts @@ -0,0 +1,18 @@ +export function merge_env(procEnv: any) { + const env = { + ...process.env, + } + + // Overwrite with user specified variables + for (const key in procEnv) { + if (procEnv.hasOwnProperty(key)) { + if (procEnv[key] === null) { + delete env[key] + } else { + env[key] = procEnv[key] + } + } + } + + return env +} \ No newline at end of file diff --git a/src/debug/backend/session/escapePath.ts b/src/debug/backend/session/escapePath.ts new file mode 100644 index 0000000..a30c0a5 --- /dev/null +++ b/src/debug/backend/session/escapePath.ts @@ -0,0 +1,7 @@ +export function escapePath(str: string) { + if (typeof str !== 'string') { + str = '' + } + str = str.replace(/\\/g, '/') + return JSON.stringify(str) +} diff --git a/src/debug/backend/session/gdb_expansion.ts b/src/debug/backend/session/gdb_expansion.ts new file mode 100644 index 0000000..c54086e --- /dev/null +++ b/src/debug/backend/session/gdb_expansion.ts @@ -0,0 +1,286 @@ +import { objectPath } from '../../common/library/objectPath' + +const resultRegex = /^([a-zA-Z_\-][a-zA-Z0-9_\-]*|\[\d+\])\s*=\s*/ +const variableRegex = /^[a-zA-Z_\-][a-zA-Z0-9_\-]*/ +const errorRegex = /^\<.+?\>/ +const referenceStringRegex = /^(0x[0-9a-fA-F]+\s*)"/ +const referenceRegex = /^0x[0-9a-fA-F]+/ +const cppReferenceRegex = /^@0x[0-9a-fA-F]+/ +const nullpointerRegex = /^0x0+\b/ +const charRegex = /^(\d+) ['"]/ +const numberRegex = /^\d+(\.\d+)?/ +const pointerCombineChar = '.' + +export function isExpandable(value: string): number { + let match + value = value.trim() + if (value.length == 0) { + return 0 + } else if (value.startsWith('{...}')) { + return 2 + }// lldb string/array + else if (value[0] == '{') { + return 1 + }// object + else if (value.startsWith('true')) { + return 0 + } else if (value.startsWith('false')) { + return 0 + } else if (match = nullpointerRegex.exec(value)) { + return 0 + } else if (match = referenceStringRegex.exec(value)) { + return 0 + } else if (match = referenceRegex.exec(value)) { + return 2 + }// reference + else if (match = charRegex.exec(value)) { + return 0 + } else if (match = numberRegex.exec(value)) { + return 0 + } else if (match = variableRegex.exec(value)) { + return 0 + } else if (match = errorRegex.exec(value)) { + return 0 + } else { + return 0 + } +} + +export function expandValue(variableCreate: Function, value: string, root: string = '', extra: any = undefined): any { + const parseCString = () => { + value = value.trim() + if (value[0] != '"' && value[0] != '\'') { + return '' + } + let stringEnd = 1 + let inString = true + const charStr = value[0] + let remaining = value.substr(1) + let escaped = false + while (inString) { + if (escaped) { + escaped = false + } else if (remaining[0] == '\\') { + escaped = true + } else if (remaining[0] == charStr) { + inString = false + } + + remaining = remaining.substr(1) + stringEnd++ + } + const str = value.substr(0, stringEnd).trim() + value = value.substr(stringEnd).trim() + return str + } + + const stack = [root] + let parseValue: { (): any; (): void; (): void; (): void; (): void; }, parseCommaResult: { (pushToStack?: boolean): any; (arg0: boolean): void; }, parseCommaValue: { (): any; (): void; }, parseResult: { (pushToStack?: boolean): any; (arg0: boolean): void; (arg0: boolean): void }, createValue: { (name: any, val: any): { name: any; value: any; variablesReference: number; }; (arg0: string, arg1: any): void; (arg0: string, arg1: any): void; (arg0: string, arg1: any): void; } + let variable = '' + + const getNamespace = (variable: string) => { + let namespace = '' + let prefix = '' + stack.push(variable) + stack.forEach(name => { + prefix = '' + if (name != '') { + if (name.startsWith('[')) { + namespace = namespace + name + } else { + if (namespace) { + while (name.startsWith('*')) { + prefix += '*' + name = name.substr(1) + } + namespace = namespace + pointerCombineChar + name + } else { + namespace = name + } + } + } + }) + stack.pop() + return prefix + namespace + } + + const parseTupleOrList = () => { + value = value.trim() + if (value[0] != '{') { + return undefined + } + const oldContent = value + value = value.substr(1).trim() + if (value[0] == '}') { + value = value.substr(1).trim() + return [] + } + if (value.startsWith('...')) { + value = value.substr(3).trim() + if (value[0] == '}') { + value = value.substr(1).trim() + return '<...>' + } + } + const eqPos = value.indexOf('=') + const newValPos1 = value.indexOf('{') + const newValPos2 = value.indexOf(',') + let newValPos = newValPos1 + if (newValPos2 != -1 && newValPos2 < newValPos1) { + newValPos = newValPos2 + } + if (newValPos != -1 && eqPos > newValPos || eqPos == -1) { // is value list + const values = [] + stack.push('[0]') + let val = parseValue() + stack.pop() + values.push(createValue('[0]', val)) + const remaining = value + let i = 0 + while (true) { + stack.push('[' + (++i) + ']') + if (!(val = parseCommaValue())) { + stack.pop() + break + } + stack.pop() + values.push(createValue('[' + i + ']', val)) + } + value = value.substr(1).trim() // } + return values + } + + let result = parseResult(true) + if (result) { + const results = [] + results.push(result) + while (result = parseCommaResult(true)) { + results.push(result) + } + value = value.substr(1).trim() // } + return results + } + + return undefined + } + + const parsePrimitive = () => { + let primitive: any + let match + value = value.trim() + if (value.length == 0) { + primitive = undefined + } else if (value.startsWith('true')) { + primitive = 'true' + value = value.substr(4).trim() + } else if (value.startsWith('false')) { + primitive = 'false' + value = value.substr(5).trim() + } else if (match = nullpointerRegex.exec(value)) { + primitive = '' + value = value.substr(match[0].length).trim() + } else if (match = referenceStringRegex.exec(value)) { + value = value.substr(match[1].length).trim() + primitive = parseCString() + } else if (match = referenceRegex.exec(value)) { + primitive = '*' + match[0] + value = value.substr(match[0].length).trim() + } else if (match = cppReferenceRegex.exec(value)) { + primitive = match[0] + value = value.substr(match[0].length).trim() + } else if (match = charRegex.exec(value)) { + primitive = match[1] + value = value.substr(match[0].length - 1) + primitive += ' ' + parseCString() + } else if (match = numberRegex.exec(value)) { + primitive = match[0] + value = value.substr(match[0].length).trim() + } else if (match = variableRegex.exec(value)) { + primitive = match[0] + value = value.substr(match[0].length).trim() + } else if (match = errorRegex.exec(value)) { + primitive = match[0] + value = value.substr(match[0].length).trim() + } else { + primitive = value + } + return primitive + } + + parseValue = () => { + value = value.trim() + if (value[0] == '"') { + return parseCString() + } else if (value[0] == '{') { + return parseTupleOrList() + } else { + return parsePrimitive() + } + } + + parseResult = (pushToStack: boolean = false) => { + value = value.trim() + const variableMatch = resultRegex.exec(value) + if (!variableMatch) { + return undefined + } + value = value.substr(variableMatch[0].length).trim() + const name = variable = variableMatch[1] + if (pushToStack) { + stack.push(variable) + } + const val = parseValue() + if (pushToStack) { + stack.pop() + } + return createValue(name, val) + } + + createValue = (name: string, val: string) => { + let ref = 0 + if (typeof val == 'object') { + ref = variableCreate(val) + val = 'Object' + } else if (typeof val == 'string' && val.startsWith('*0x')) { + if (extra && objectPath(extra, 'arg') == '1') { + ref = variableCreate(getNamespace('*(' + name), { arg: true }) + val = '' + } else { + ref = variableCreate(getNamespace('*' + name)) + val = 'Object@' + val + } + } else if (typeof val == 'string' && val.startsWith('@0x')) { + ref = variableCreate(getNamespace('*&' + name.substr)) + val = 'Ref' + val + } else if (typeof val == 'string' && val.startsWith('<...>')) { + ref = variableCreate(getNamespace(name)) + val = '...' + } + return { + name: name, + value: val, + variablesReference: ref, + } + } + + parseCommaValue = () => { + value = value.trim() + if (value[0] != ',') { + return undefined + } + value = value.substr(1).trim() + return parseValue() + } + + parseCommaResult = (pushToStack: boolean = false) => { + value = value.trim() + if (value[0] != ',') { + return undefined + } + value = value.substr(1).trim() + return parseResult(pushToStack) + } + + value = value.trim() + return parseValue() +} diff --git a/src/debug/backend/session/session.ts b/src/debug/backend/session/session.ts new file mode 100644 index 0000000..1812993 --- /dev/null +++ b/src/debug/backend/session/session.ts @@ -0,0 +1,550 @@ +import { existsSync, mkdirSync, unlink } from 'fs' +import { createServer, Server } from 'net' +import { tmpdir } from 'os' +import * as systemPath from 'path' +import { BreakpointEvent, ContinuedEvent, TerminatedEvent, ThreadEvent } from 'vscode-debugadapter' +import { DebugProtocol } from 'vscode-debugprotocol' +import { IMyLogger } from '@utils/baseLogger' +import { createGdbProcess, IChildProcess, waitProcess } from '@debug/common/createGdbProcess' +import { DeferredPromise, sleep } from '@debug/common/deferredPromise' +import { Emitter } from '@debug/common/event' +import { errorMessage, padPercent } from '@debug/common/library/strings' +import { BreakPointController, IBreakpointDiff } from '@debug/common/mi2/breakPointController' +import { IRunStateEvent, IThreadEvent, Mi2AutomaticResponder, ThreadNotify } from '@debug/common/mi2/mi2AutomaticResponder' +import { createStopEvent, StopReason } from '@debug/common/mi2/pause' +import { BreakpointType, IMyStack, IMyThread, IMyVariable, MyBreakpoint } from '@debug/common/mi2/types' +import { MESSAGE_LOADING_PROGRAM } from '@debug/messages' +import { IGDBStack } from '@debug/backend/gdbDataStructs/stack' +import { IGDBThread } from '@debug/backend/gdbDataStructs/thread' +import { IGDBVariable } from '@debug/backend/gdbDataStructs/variable' +import { IDebugConsole } from '@debug/backend/lib/duplexDebugConsole' +import { AttachRequestArguments, LaunchRequestArguments, VariableObject } from '@debug/backend/type' +import split2 = require('split2') + + +export class DebuggingSession { + private readonly handler: Mi2AutomaticResponder + + private readonly processToExit: Promise + private readonly connectReady: DeferredPromise + + private readonly _onEvent = new Emitter() + public readonly onEvent = this._onEvent.event + private readonly commandServer: Server + private readonly commandPath: string + private readonly breakpoints = new BreakPointController() + private readonly process: IChildProcess + + constructor( + private readonly config: AttachRequestArguments | LaunchRequestArguments, + private readonly logger: IMyLogger, + private readonly debugConsole: IDebugConsole, + ) { + this.connectReady = new DeferredPromise() + + /* PROCESS */ + const process = this.process = createGdbProcess({ + gdb: config.gdbpath, + app: config.executable, + args: config.debuggerArgs, + env: config.env, + logger: this.logger, + }) + this.processToExit = waitProcess(process).catch((e) => { + this.debugConsole.errorUser('process return error: ' + e.toString()) + this.debugConsole.errorUser('ignore this process.') + }).finally(() => { + this.debugConsole.logUser('debug session finished.') + this.triggerEvent(new TerminatedEvent()) + }) + + process.stderr.pipe(split2()).on('data', (line: Buffer) => { + this.debugConsole.errorUser(line.toString('utf8')) + }) + + /* COMMAND SERVER */ + const tempPath = systemPath.join(tmpdir(), 'kendryte-debug-sockets') + this.commandPath = systemPath.join(tempPath, 'debug-' + Math.floor(Math.random() * 36 * 36 * 36 * 36).toString(36)) + + const commandServer = createServer(c => { + c.on('data', data => { + const rawCmd = data.toString() + const spaceIndex = rawCmd.indexOf(' ') + let func = rawCmd + let args = [] + if (spaceIndex != -1) { + func = rawCmd.substr(0, spaceIndex) + args = JSON.parse(rawCmd.substr(spaceIndex + 1)) + } + interface IParams { + [propName: string]: any + } + Promise.resolve((this)[func].apply(this, args)).then(data => { + c.write(data.toString()) + }) + }) + }) + commandServer.on('error', err => { + this.logger.error('Kendryte-Debug WARNING: Utility Command Server: Error in command socket ' + err.toString()) + }) + if (!existsSync(tempPath)) { + mkdirSync(tempPath) + } + commandServer.listen() + + this.commandServer = commandServer + + /* FINAL */ + this.handler = new Mi2AutomaticResponder(process, this.logger) + + this.registerMi2EventHandlers() + } + + get isRunning() { + return this.handler.isRunning + } + + async dispose() { + const proc = this.process + if (!proc) { + return + } + + this.handler.commandEnsure('gdb-exit') + + const to = setTimeout(() => { + this.logger.error('exit timeout, force kill.') + proc.kill('SIGKILL') + }, 4000) + + await this.disconnected + clearTimeout(to) + + this._onEvent.dispose() + this.handler.dispose() + + await new Promise((resolve) => { + this.commandServer.close(() => { + unlink(this.commandPath, (err) => { + if (err) { + console.error(err) + } + resolve() + }) + }) + }) + } + + async load() { + this.debugConsole.logUser(MESSAGE_LOADING_PROGRAM) + // await this.handler.command('exec-interrupt') + let totalSent = 0, totalSize = NaN + await this.handler.commandEnsure('target-download').progress((node) => { + const section = node.result('section') + const sectionSize = parseInt(node.result('section-size')) + const sectionSent = parseInt(node.result('section-sent')) + let sectionProgress = '.' + if (!isNaN(sectionSize)) { + if (!isNaN(sectionSent)) { + const percent = ((100 * sectionSent / sectionSize).toFixed(0)) + sectionProgress = `: (${percent}%) ${sectionSent}/${sectionSize} ...` + } else { + sectionProgress = ': size = ' + sectionSize + } + } + + totalSent = parseInt(node.result('total-sent')) || totalSent + totalSize = parseInt(node.result('total-size')) || totalSize + + const percent = padPercent(100 * totalSent / totalSize) + + this.debugConsole.logUser(`[${percent}] ${totalSent}/${totalSize}, Section "${section}" ${sectionProgress}`) + }) + this.debugConsole.logUser('program loaded.') + } + + async examineMemory(from: number, length: number) { + const result = await this.handler.commandEnsure('data-read-memory-bytes', '0x' + from.toString(16), length.toString(10)) + // this.logger.info('request: examineMemory(%s, %s)', from, length, result) + return result.result('memory[0].contents') + } + + setBreakPointCondition(bkptNum: string, condition: string) { + // this.logger.debug('request: setBreakPointCondition()') + return this.handler.commandEnsure('break-condition', bkptNum, condition) + } + + async removeBreakPoint(file: string, breakpoint: MyBreakpoint) { + if (!breakpoint.gdbBreakNum) { + throw new Error('removing non-exists breakpoint.') + } + const result = await this.handler.commandEnsure('break-delete', breakpoint.gdbBreakNum.toString()) + + if (result.className !== 'done') { + throw new Error('cannot remove breakpoint ' + breakpoint.gdbBreakNum) + } + + this.debugConsole.errorUser(`Delete breakpoint: ${breakpoint.gdbBreakNum || ''}`) + this.breakpoints.removeExists(file, breakpoint) + + delete breakpoint.gdbBreakNum + return breakpoint + } + + private triggerEvent(e: DebugProtocol.Event) { + delete e.seq + this._onEvent.fire(e) + } + + private async addBreakPoint(file: string, breakpoint: MyBreakpoint): Promise { + if (breakpoint.gdbBreakNum) { + throw new Error('adding registered breakpoint.') + } + + let special: string[] = [] + if (breakpoint.type === BreakpointType.Line && breakpoint.logMessage) { + special.push('-a') + } + if (breakpoint.condition) { + special.push('-c', JSON.stringify(breakpoint.condition)) + } + + breakpoint.tried = true + const result = breakpoint.type === BreakpointType.Line ? + await this.handler.commandEnsure('break-insert', '--source', JSON.stringify(breakpoint.file), '--line', breakpoint.line.toString(), ...special) : + await this.handler.commandEnsure('break-insert', '--function', breakpoint.name, ...special) + + breakpoint.gdbBreakNum = parseInt(result.result('bkpt.number') || result.result('bkpt.MI2ChildValues.0.number')) + + let resultFile: string + let line: number + breakpoint.addr = result.result('bkpt.addr') + if (breakpoint.addr === '') { + breakpoint.addr = result.result('bkpt.MI2ChildValues.0.addr') + resultFile = result.result('bkpt.MI2ChildValues.0.file') + line = parseInt(result.result('bkpt.MI2ChildValues.0.line')) + } else { + resultFile = result.result('bkpt.file') + line = parseInt(result.result('bkpt.line')) + } + + const sameAddress = this.breakpoints.conflictsAddress(breakpoint.addr || '') + if (sameAddress) { + this.logger.info('add more breakpoints on same address: a1: %s, a2:%s , id=%s', sameAddress.addr, breakpoint.addr, breakpoint.gdbBreakNum) + this.triggerEvent(new BreakpointEvent('removed', { id: breakpoint.gdbBreakNum } as any)) + return sameAddress + } + + if (breakpoint.type === BreakpointType.Line) { + breakpoint.file = resultFile + breakpoint.line = line + } + + const base = systemPath.basename(resultFile) + this.debugConsole.errorUser(`New breakpoint: ${breakpoint.gdbBreakNum} (${result.result('bkpt.func')} in ${base}:${line})`) + this.breakpoints.registerNew(file, breakpoint) + return breakpoint + } + + private async modifyBreakPoint(breakpoint: MyBreakpoint, change: IBreakpointDiff) { + if (change.condition) { + await this.handler.commandEnsure('break-condition', breakpoint.gdbBreakNum ? breakpoint.gdbBreakNum.toString() : '', breakpoint.condition) + } + return breakpoint + } + + /* EVENTS */ + private registerMi2EventHandlers() { + this.handler.onSimpleLine(({ error, message }) => { + if (error) { + this.debugConsole.error(message) + } else { + this.debugConsole.log(message) + } + }) + this.handler.onThreadNotify(this.threadEvent.bind(this)) + this.handler.onTargetRunStateChange(this.handleStopResume.bind(this)) + } + + private handleStopResume(status: IRunStateEvent) { + if (status.realChange) { + this.debugConsole.errorUser(status.running ? '> continue' : '> interrupt by ' + status.reasonString) + + if (status.running) { + const event = new ContinuedEvent(status.threadId, status.allThreads) + this.triggerEvent(event) + } else { + const event = createStopEvent(status.reason, status.threadId) + event.body.allThreadsStopped = status.allThreads + event.body.preserveFocusHint = false + this.triggerEvent(event) + } + } + } + + private threadEvent(event: IThreadEvent) { + if (event.type === ThreadNotify.Created) { + this.triggerEvent(new ThreadEvent('started', event.id)) + } else { + this.triggerEvent(new ThreadEvent('exited', event.id)) + } + } + + private forceStatusEvent(reason: StopReason = StopReason.UnknownReason) { + if (this.handler.isRunning) { + this.triggerEvent(new ContinuedEvent(0, true)) + } else { + this.triggerEvent(createStopEvent(reason, undefined, 'force refresh status')) + } + } + + get connected() { + return this.connectReady.p + } + + get disconnected() { + return this.processToExit.catch(() => { + }).then(() => { + return this + }) + } + + public async connect(load: boolean) { + if (this.connectReady.isFired()) { + throw new Error('Already connected') + } + + this.debugConsole.logUser('[kendryte debug] debugger starting...') + this.logger.info(`[kendryte debug] debugger starting: test log.`) + + await this.handler.commandSequence([ + ['gdb-set', 'target-async', 'on'], + ['target-select', 'remote', this.config.target], + ]).then(() => { + this.connectReady.complete() + }, (e: Error) => { + this.connectReady.error(e) + throw e + }) + this.debugConsole.logUser('connected to: ' + this.config.target) + + if (load) { + await this.load() + } + } + + /* REQUESTS */ + async reload() { + await this.interrupt() + return this.load() + } + + detach(): Promise { + const proc = this.process + const [to, dispose] = sleep(3000) + to.then(() => { + this.logger.warning('detach timeout, force kill.') + proc.kill('SIGKILL') + }) + this.process.on('exit', function () { + dispose() + }) + return Promise.race([ + this.handler.commandEnsure('target-detach'), + to, + ]) + } + + public async terminate() { + this.debugConsole.logUser('[kendryte debug] debugger stopping.') + await this.dispose() + await this.processToExit + this.debugConsole.logUser('ok.') + } + + async interrupt() { + this.logger.info('request interrupt, current state is: %s', this.handler.isRunning) + + await this.handler.execInterrupt() + await this.handler.waitInterrupt() + } + + async continue() { + await this.handler.commandEnsure('exec-continue') + if (!this.handler.isRunning) { + this.forceStatusEvent() + throw new Error('request continue, but program not run.') + } + } + + async next() { + await this.handler.commandEnsure('exec-next') + await this.handler.waitInterrupt() + } + + async step() { + await this.handler.commandEnsure('exec-step') + await this.handler.waitInterrupt() + } + + async stepOut() { + await this.handler.commandEnsure('exec-finish') + await this.handler.waitInterrupt() + } + + async updateBreakPoints(file: string, breakpoints: MyBreakpoint[]) { + const ret: MyBreakpoint[] = [] + for (const breakpoint of breakpoints) { + let exists = this.breakpoints.isExists(file, breakpoint) + if (exists) { + const diff = this.breakpoints.compareBreakpoint(exists, breakpoint) + + if (!diff) { + this.logger.info('breakpoint %s is not change.', exists.gdbBreakNum) + ret.push(exists) + continue + } else { + this.logger.info('breakpoint %s is change: %s', exists.gdbBreakNum, JSON.stringify(breakpoint)) + const newBreak = await this.modifyBreakPoint(breakpoint, diff as IBreakpointDiff) + ret.push(newBreak) + continue + } + } + + this.logger.info('will create breakpoint: %s', JSON.stringify(breakpoint)) + // const newBreaks = await + const newBreak = await this.addBreakPoint(file, breakpoint).catch((err) => { + breakpoint.errorMessage = 'Cannot add breakpoint! // TODO' + this.debugConsole.errorUser('Add breakpoint failed: ' + errorMessage(err)) + + return breakpoint + }) + + ret.push(newBreak) + } + + const willDelete = this.breakpoints.filterOthers(file, ret) + this.logger.warning('will delete %s breakpoints: %s', willDelete.length, willDelete.map(i => i.gdbBreakNum).join(', ')) + for (const oldBreak of willDelete) { + await this.removeBreakPoint(file, oldBreak) + } + + const dump = this.breakpoints.dump(file) + this.logger.warning('current file %s has %s breakpoints:\n%s', file, dump.length, dump.map((b) => { + return ` #${b.gdbBreakNum} - address:${b.addr}` + })) + + return ret + } + + async getThreads(required: boolean = true): Promise { + const result = required ? + await this.handler.commandEnsure('thread-info') : + await this.handler.command('thread-info') + const threads = result.result('threads') + return threads.map(element => { + const ret: IMyThread = { + id: parseInt(element.id), + targetId: element['target-id'], + } + + const name = element.name + if (name) { + ret.name = name + } + + return ret + }) + } + + async getStack(maxLevels: number, thread: number): Promise { + let command = 'stack-list-frames' + if (thread != 0) { + command += ` --thread ${thread}` + } + if (maxLevels) { + command += ' 0 ' + maxLevels + } + const result = await this.handler.commandEnsure(command) + const stacks = result.result('stack') + + return stacks.map(({ frame }) => { + return { + address: frame.addr, + fileName: frame.file, + file: frame.fullname, + function: frame.func || frame.from, + level: parseInt(frame.level), + line: parseInt(frame.line || '0'), + } + }) + } + + async getStackVariables(thread: number, frame: number): Promise { + const result = await this.handler.commandEnsure('stack-list-variables', '--thread', thread.toString(), '--frame', frame.toString(), '--simple-values') + + const variables = result.result('variables') + return variables.map((element) => { + return { + name: element.name, + valueStr: element.value, + type: element.type, + raw: element, + } + }) + } + + async varAssign(name: string, rawValue: string) { + const res = await this.handler.commandEnsure('var-assign', name, rawValue) + // this.logger.debug('request: varAssign(%s,%s)', name, rawValue, res) + return res.result('value') + } + + varUpdate(varObjName: string) { + return this.handler.commandEnsure('var-list-children', '--all-values', varObjName) + } + + async varCreate(expression: string, name: string = '-'): Promise { + const res = await this.handler.commandEnsure('var-create', name, '@', JSON.stringify(expression)) + return new VariableObject(res.result('')) + } + + async varListChildren(name: string): Promise { + // this.logger.debug('request: varListChildren()') + //TODO: add `from` and `to` arguments + const res = await this.handler.commandEnsure('var-list-children', '--all-values', name) + const children = res.result('children') || [] + return children.map((child: any[]) => new VariableObject(child[1])) + } + + async changeVariable(name: string, rawValue: string) { + await this.handler.commandEnsure('gdb-set', 'var', name + '=' + rawValue) + // this.logger.debug('request: changeVariable(%s, %s)', name, rawValue) + return rawValue + } + + public sendUserInput(expression: string, threadId: number, frameLevel: number) { + switch (expression) { + case '!fe': + this.forceStatusEvent() + return + } + if (expression.startsWith('-')) { + return this.handler.command(expression.substr(1)) + } else { + return this.handler.cliCommand(expression, threadId, frameLevel) + } + } + + evalExpression(name: string, thread: number, frame: number) { + // this.logger.debug('request: evalExpression(%s, %d, %d)', name, thread, frame) + + const args = [] + if (thread != 0) { + args.push('--thread', thread.toString(), '--frame', frame.toString()) + } + args.push(name) + + return this.handler.commandEnsure('data-evaluate-expression', ...args) + } +} diff --git a/src/debug/backend/type.ts b/src/debug/backend/type.ts new file mode 100644 index 0000000..e046e21 --- /dev/null +++ b/src/debug/backend/type.ts @@ -0,0 +1,76 @@ +import { DebugProtocol } from 'vscode-debugprotocol/lib/debugProtocol' +import { objectPath } from '../common/library/objectPath' +import { MINode } from '../common/mi2/mi2Node' + +export interface RequestArguments { + id: string + cwd: string + target: string + gdbpath: string + env: NodeJS.ProcessEnv + debuggerArgs: string[] + autorun: string[] + executable: string + remote: boolean + valuesFormatting: ValuesFormattingMode +} + +export interface LaunchRequestArguments extends RequestArguments, DebugProtocol.LaunchRequestArguments { +} + +export interface AttachRequestArguments extends RequestArguments, DebugProtocol.AttachRequestArguments { +} + +export type ValuesFormattingMode = 'disabled' | 'parseText' | 'prettyPrinters' + +export class VariableObject { + name: string + exp: string + numchild: number + type: string + value: string + threadId: string + frozen: boolean + dynamic: boolean + displayhint: string + hasMore: boolean + id!: number + + constructor(node: any) { + this.name = objectPath(node, 'name') + this.exp = objectPath(node, 'exp') + this.numchild = parseInt(objectPath(node, 'numchild')) + this.type = objectPath(node, 'type') + this.value = objectPath(node, 'value') + this.threadId = objectPath(node, 'thread-id') + this.frozen = !!objectPath(node, 'frozen') + this.dynamic = !!objectPath(node, 'dynamic') + this.displayhint = objectPath(node, 'displayhint') + // TODO: use has_more when it's > 0 + this.hasMore = !!objectPath(node, 'has_more') + } + + public applyChanges(node: MINode) { + this.value = objectPath(node, 'value') + if (!!objectPath(node, 'type_changed')) { + this.type = objectPath(node, 'new_type') + } + this.dynamic = !!objectPath(node, 'dynamic') + this.displayhint = objectPath(node, 'displayhint') + this.hasMore = !!objectPath(node, 'has_more') + } + + public isCompound(): boolean { + return this.numchild > 0 || this.value === '{...}' || (this.dynamic && (this.displayhint === 'array' || this.displayhint === 'map')) + } + + public toProtocolVariable(): DebugProtocol.Variable { + return { + name: this.exp, + evaluateName: this.name, + value: (this.value === void 0) ? '' : this.value, + type: this.type, + variablesReference: this.id, + } + } +} diff --git a/src/debug/common/createGdbProcess.ts b/src/debug/common/createGdbProcess.ts new file mode 100644 index 0000000..176116d --- /dev/null +++ b/src/debug/common/createGdbProcess.ts @@ -0,0 +1,50 @@ +import { ChildProcess, spawn } from 'child_process' +import { resolve } from 'path' +import { IMyLogger } from '@utils/baseLogger' + +export interface ISpawnArguments { + gdb: string + app: string + env: NodeJS.ProcessEnv + args: string[] + logger: IMyLogger +} + +export interface IChildProcess extends ChildProcess { +} + +export function createGdbProcess(arg: ISpawnArguments): IChildProcess { + const logger = arg.logger + const args = ['--interpreter=mi2', '--quiet'] + if (arg.args) { + args.push(...arg.args) + } + + logger.info(`gdb: ${arg.gdb}`) + logger.info(`app: ${arg.app}`) + logger.info(`args: ${args.join(', ')}`) + logger.debug(`env: ${JSON.stringify(arg.env, null, 8)}`) + const process = spawn(arg.gdb, [arg.app, ...args], { + cwd: resolve(arg.app, '..'), + env: arg.env, + stdio: 'pipe', + shell: false, + windowsHide: true, + }) + logger.info(`pid: ${process.pid}`) + + return process +} + +export function waitProcess(p: ChildProcess) { + return new Promise((resolve, reject) => { + p.on('error', reject) + p.on('exit', (code, signal) => { + if (code === 0) { + resolve() + } else { + reject(new Error(`child process exit with code ${code || '?'}, signal ${signal || '?'}`)) + } + }) + }) +} diff --git a/src/debug/common/deferredPromise.ts b/src/debug/common/deferredPromise.ts new file mode 100644 index 0000000..5249aa2 --- /dev/null +++ b/src/debug/common/deferredPromise.ts @@ -0,0 +1,107 @@ +export function sleep(ms: number): [Promise, () => void] { + let cb = () => {} + const p = new Promise((resolve, reject) => { + const to = setTimeout(() => resolve(), ms) + cb = () => { + clearTimeout(to) + reject(canceled()) + } + }) + return [p, cb] +} + +export function timeout(ms: number): [Promise, () => void] { + let cb = () => {} + const p = new Promise((resolve, reject) => { + const to = setTimeout(() => reject(canceled()), ms) + cb = () => { + clearTimeout(to) + resolve() + } + }) + return [p, cb] +} + +export interface ProgressPromise extends Promise { + progress(cb: NotifyCallback): this +} + +export class DeferredPromise { + public readonly p: ProgressPromise + private completeCallback!: ValueCallback + private errorCallback!: (err: any) => void + private notifyCallbacks: NotifyCallback[] = [] + private _isComplete!: boolean + private _isError!: boolean + + constructor() { + this.p = Object.assign(new Promise((c, e) => { + this.completeCallback = (d) => { + this._isComplete = true + c(d) + } + this.errorCallback = (err) => { + this._isError = true + e(err) + } + }), { + progress: (cb: NotifyCallback) => { + this.notifyCallbacks.push(cb) + return this.p + }, + }) + } + + public complete(value: T) { + process.nextTick(() => { + this.completeCallback(value) + }) + } + + public error(err: any) { + process.nextTick(() => { + this.errorCallback(err) + }) + } + + public cancel() { + process.nextTick(() => { + this.errorCallback(canceled()) + }) + } + + public notify(data: NT) { + for (const cb of this.notifyCallbacks) { + cb(data) + } + } + + public isFired() { + return this._isComplete || this._isError + } +} + +export type ValueCallback = (value: T | Thenable) => void +export type NotifyCallback = (value: T) => void + +const canceledName = 'Canceled' +const timoutName = 'Timeout' + +export class CanceledError extends Error { +} + +export function timeouts(): CanceledError { + const error = new CanceledError(timoutName) + error.name = timoutName + return error +} + +export function canceled(): CanceledError { + const error = new CanceledError(canceledName) + error.name = canceledName + return error +} + +export function isCancel(e: Error) { + return e instanceof CanceledError +} diff --git a/src/debug/common/env.ts b/src/debug/common/env.ts new file mode 100644 index 0000000..acc1206 --- /dev/null +++ b/src/debug/common/env.ts @@ -0,0 +1,4 @@ +import { platform } from 'os' + +export const isWin = platform() === 'win32' +export const executableExtension = isWin ? '.exe' : '' diff --git a/src/debug/common/event.ts b/src/debug/common/event.ts new file mode 100644 index 0000000..6efaf57 --- /dev/null +++ b/src/debug/common/event.ts @@ -0,0 +1,39 @@ +import { EventEmitter } from 'events' + +export interface EventCallback { + (data: T): void +} + +export interface EventRegister { + (cb: EventCallback): void +} + +export class Emitter { + private nodeEvent = new EventEmitter() + private _enabled: boolean = true + + pause() { + this._enabled = false + } + + resume() { + this._enabled = true + } + + fire(data: T) { + if (this._enabled) { + this.nodeEvent.emit('rawEvent', data) + } + } + + dispose() { + this.nodeEvent.removeAllListeners('rawEvent') + delete this.nodeEvent + } + + get event(): EventRegister { + return (cb: EventCallback) => { + this.nodeEvent.on('rawEvent', cb) + } + } +} \ No newline at end of file diff --git a/src/debug/common/eventProtocol.ts b/src/debug/common/eventProtocol.ts new file mode 100644 index 0000000..be6c1f5 --- /dev/null +++ b/src/debug/common/eventProtocol.ts @@ -0,0 +1,16 @@ +import { LogLevel } from '@utils/baseLogger' + +export interface Body { + [id: string]: any +} + +export interface ICustomEvent { + type: string + event: BODY +} + +export interface ILogEventBody { + level: LogLevel + message: string + args?: string[] +} \ No newline at end of file diff --git a/src/debug/common/guid.ts b/src/debug/common/guid.ts new file mode 100644 index 0000000..ef718e3 --- /dev/null +++ b/src/debug/common/guid.ts @@ -0,0 +1,13 @@ +export function autoIncrease() { + let current = 1 + return { + get current() { + return current + }, + next() { + current++ + return current + }, + } +} + diff --git a/src/debug/common/library/objectPath.ts b/src/debug/common/library/objectPath.ts new file mode 100644 index 0000000..71e7a21 --- /dev/null +++ b/src/debug/common/library/objectPath.ts @@ -0,0 +1,18 @@ +export function objectPath(obj: object, path: string): any { + path.split('.').every((name) => { + return obj = (obj as any)[name] + }) + return obj +} + +export function skipArray(item: ReadonlyArray, path: string) { + if (!Array.isArray(item) || item.length === 0) { + return undefined + } + let ret: any = undefined, i = 0 + item.some((item) => { + return ret = objectPath(item, path) + }) + + return ret +} diff --git a/src/debug/common/library/speedShow.ts b/src/debug/common/library/speedShow.ts new file mode 100644 index 0000000..363b8cf --- /dev/null +++ b/src/debug/common/library/speedShow.ts @@ -0,0 +1,82 @@ +import { MESSAGE_DOWNLOAD_SPEED } from '../../messages' + +export function showDownloadSpeed(total: number, workedStart: number = 0) { + const startAt = Date.now() + + return function (worked: number): string { + if (worked === 0) { + return '...' + } + const kbps = (worked - workedStart) / (Date.now() - startAt) + const kbpsStr = isNaN(kbps) ? 'Unknown' : kbps.toFixed(1) + 'KB/s' + + const complete = 100 * worked / total + const completeStr = isNaN(complete) ? 'Unknown' : complete.toFixed(1) + '%' + + const speedLevel = emojiByLevel(kbps) + + return MESSAGE_DOWNLOAD_SPEED(completeStr, speedLevel, kbpsStr) + } +} + +function emojiByLevel(bps: number) { + if (isNaN(bps)) { + return '\uD83E\uDD14' + } + if (bps < 20) { // 10K + return '\uD83D\uDC0C' + } else if (bps < 100) { // 100K + return '\uD83D\uDEB2' + } else if (bps < 1000) { // 1M + return '\uD83D\uDE97' + } else if (bps < 20000) { // 20M + return '\u2708\uFE0F' + } else { + return '\uD83D\uDE80' + } +} + +export function humanSize(bytes: number, fixed = 2) { + if (bytes < 1024) { + return bytes + 'B' + } else if (bytes < 1048576) { // 1024 * 1024 + return (bytes / 1024).toFixed(fixed) + 'KB' + } else if (bytes < 1073741824) { // 1024 * 1024 * 1024 + return (bytes / 1048576).toFixed(fixed) + 'MB' + } else { + return (bytes / 1073741824).toFixed(fixed) + 'GB' + } +} + +export function humanSpeed(bps: number, fixed = 2) { + return humanSize(bps) + '/s' +} + +export class SpeedMeter { + private startTime!: number + private deltaTime!: number + private currentBytes!: number + + constructor() { + } + + public getSpeed() { + if (this.deltaTime) { + return humanSpeed(1000 * this.currentBytes / this.deltaTime) + } else { + return humanSpeed(1000 * this.currentBytes / (Date.now() - this.startTime)) + } + } + + public setCurrent(currentBytes: number) { + this.currentBytes = currentBytes + } + + public start() { + this.startTime = Date.now() + } + + public complete() { + this.deltaTime = Date.now() - this.startTime + } +} diff --git a/src/debug/common/library/strings.ts b/src/debug/common/library/strings.ts new file mode 100644 index 0000000..5c9e04f --- /dev/null +++ b/src/debug/common/library/strings.ts @@ -0,0 +1,23 @@ +export function padPercent(n: number) { + const s = n.toFixed(0) + if (s.length === 3) { + return s + '%' + } else if (s.length === 2) { + return ' ' + s + '%' + } else if (s.length === 1) { + return ' ' + s + '%' + } + return 'NaN' +} + +export function errorMessage(e: Error): string { + return e ? e.message || '' + e || 'UnknownError' : 'UnknownError' +} + +export function errorStack(e: Error) { + return e && e.stack || errorMessage(e) + '\n No stack trace available' +} + +export function dumpJson(a: any) { + return `\n--------------------------------\n${JSON.stringify(a, null, 4)}\n--------------------------------` +} diff --git a/src/debug/common/mi2/breakPointController.ts b/src/debug/common/mi2/breakPointController.ts new file mode 100644 index 0000000..9610104 --- /dev/null +++ b/src/debug/common/mi2/breakPointController.ts @@ -0,0 +1,92 @@ +import { BreakpointType, MyBreakpoint, MyBreakpointFunc, MyBreakpointLine } from './types' + +export interface IBreakpointDiff { + condition?: boolean + logMessage?: boolean +} + +export class BreakPointController { + private readonly bp = new Map() + + constructor() { + } + + public registerNew(category: string, breakpoint: MyBreakpoint) { + if (!this.bp.has(category)) { + this.bp.set(category, []) + } + (this.bp.get(category) || []).push(breakpoint) + } + + public removeExists(category: string, breakpoint: MyBreakpoint) { + const arr = this.bp.get(category) || [] + const index = arr.findIndex((item) => { + return isSame(item, breakpoint) + }) + arr.splice(index, 1) + } + + public compareBreakpoint(a: MyBreakpoint, b: MyBreakpoint): IBreakpointDiff | false { + let ret: IBreakpointDiff = {} + if (isSame(a, b)) { + if (a.type === BreakpointType.Line && b.type === BreakpointType.Line) { + ret.logMessage = a.logMessage !== b.logMessage + } + } + ret.condition = a.condition !== b.condition + + if (ret.logMessage || ret.condition) { + return ret // update + } + return false // no change + } + + public isExists(file: string, breakpoint: MyBreakpoint): MyBreakpoint | undefined { + if (!this.bp.has(file)) { + return + } + return (this.bp.get(file) || []).find((item) => { + return isSame(breakpoint, item) + }) + } + + conflictsAddress(addr: string): MyBreakpoint | void { + for (const arr of this.bp.values()) { + const exists = arr.find(item => item.addr === addr) + if (exists) { + return exists + } + } + } + + public filterOthers(file: string, breakpoints: MyBreakpoint[]): MyBreakpoint[] { + const ret: MyBreakpoint[] = [] + if (!this.bp.has(file)) { + return ret + } + + return (this.bp.get(file) || []).filter((exists) => { + return breakpoints.every((item) => { + return !isSame(item, exists) + }) + }) + } + + public dump(file: string): (MyBreakpoint)[] { + const ret = [] + + ret.push(...(this.bp.get(file) || [])) + + return ret + } +} + +function isSame(a: MyBreakpoint, b: MyBreakpoint) { + if (a.type === BreakpointType.Function && b.type === BreakpointType.Function && a.name === b.name) { + return true + } else if (a.type === BreakpointType.Line && b.type === BreakpointType.Line && a.file === b.file && a.line === b.line) { + return true + } else { + return false + } +} diff --git a/src/debug/common/mi2/cString.ts b/src/debug/common/mi2/cString.ts new file mode 100644 index 0000000..c047207 --- /dev/null +++ b/src/debug/common/mi2/cString.ts @@ -0,0 +1,35 @@ +const escapeChar = Buffer.from('\\')[0] +const escapeMap: { [id: string]: string } = { + /// https://en.wikipedia.org/wiki/Escape_sequences_in_C#Table_of_escape_sequences + 'a': Buffer.from('07', 'hex').toString('latin1'), + 'b': Buffer.from('08', 'hex').toString('latin1'), + 'f': Buffer.from('0C', 'hex').toString('latin1'), + 'n': Buffer.from('0A', 'hex').toString('latin1'), + 'r': Buffer.from('0D', 'hex').toString('latin1'), + 't': Buffer.from('09', 'hex').toString('latin1'), + 'v': Buffer.from('0B', 'hex').toString('latin1'), + '\\': Buffer.from('5C', 'hex').toString('latin1'), + '\'': Buffer.from('27', 'hex').toString('latin1'), + '"': Buffer.from('22', 'hex').toString('latin1'), + '?': Buffer.from('3F', 'hex').toString('latin1'), + 'e': Buffer.from('1B', 'hex').toString('latin1'), +} + +// not cover: double-quote inside the str +// like: "aaaaaa"bbbbb" - should invalid, but valid here +export function escapeCString(str: string) { + if (str[0] != '"' || str[str.length - 1] != '"') { + throw new Error('Not a valid string') + } + return str.substr(1, str.length - 2).replace(/\\u([0-9a-fA-F]{4})/g, (m, code) => { + return Buffer.from(code, 'hex').toString('latin1') + }).replace(/\\U([0-9a-fA-F]{8})/ig, (m, code) => { + return Buffer.from(code, 'hex').toString('latin1') + }).replace(/\\([0-7]{3})/ig, (m, code) => { + return Buffer.alloc(1, parseInt(code, 8)).toString('latin1') + }).replace(/\\x([0-9a-fA-F]{2})/ig, (m, code) => { + return Buffer.from(code, 'hex').toString('latin1') + }).replace(/\\([eabfnrtv\\'"?])/ig, (m, code) => { + return escapeMap[code] || m + }) +} diff --git a/src/debug/common/mi2/mi2AutomaticResponder.ts b/src/debug/common/mi2/mi2AutomaticResponder.ts new file mode 100644 index 0000000..0be9a24 --- /dev/null +++ b/src/debug/common/mi2/mi2AutomaticResponder.ts @@ -0,0 +1,342 @@ +import { ErrorMi2 } from '@debug/backend/lib/handleMethodPromise' +import { IMyLogger } from '@utils/baseLogger' +import { IChildProcess } from '../createGdbProcess' +import { DeferredPromise, isCancel, sleep, timeout } from '../deferredPromise' +import { Emitter, EventRegister } from '../event' +import { dumpJson } from '../library/strings' +import { ISimpleOutput, Mi2CommandController } from './mi2CommandController' +import { IAsyncNode, IResultNode, MINode, OneOfMiNodeType } from './mi2Node' +import { MiOutputType } from './mi2Parser' +import { StopReason } from './pause' + +const numRegex = /\d+/ +const interruptedRegex = /Interrupt./ + +export enum ThreadNotify { + Created, + Exited, + GroupAdd, + GroupStarted, + GroupExited, +} + +export interface IThreadEvent { + type: ThreadNotify + id: number +} + +export interface IRunStateEvent { + realChange: boolean + running: boolean + reason: StopReason + reasonString?: string + allThreads: boolean + threadId: number +} + +const stateErr = /Cannot execute this command while the selected thread is running|Cannot execute this command while the target is running/ + +export function isCommandIssueWhenRunning(err: Error) { + return err instanceof ErrorMi2 && stateErr.test(err.message) +} + +/** + * 调试器抽象层,不要依赖【任何】vscode相关的东西,尤其是debug session + */ +export class Mi2AutomaticResponder { + public readonly onSimpleLine: EventRegister + private controller: Mi2CommandController + private readonly _onTargetRunStateChange = new Emitter() + public readonly onTargetRunStateChange = this._onTargetRunStateChange.event + private readonly _onThreadNotify = new Emitter() + public readonly onThreadNotify = this._onThreadNotify.event + private firstTimeStop: boolean = true + + private _isRunning!: boolean + private awaitingInterrupt?: DeferredPromise + private awaitingContinue?: DeferredPromise + + constructor( + private readonly process: IChildProcess, + private readonly logger: IMyLogger, + ) { + this.controller = new Mi2CommandController(process.stdin, process.stdout, logger) + + this.onSimpleLine = this.controller.onSimpleLine + this.controller.onSimpleLine(({ error, message }) => { + logger.writeln('simpleOut: ' + message.trim()) + }) + this.controller.onReceiveMi2((node) => { + node.handled = this.handleMI(node) + }) + } + + get isRunning() { + return this._isRunning + } + + dispose() { + this.controller.dispose() + this._onTargetRunStateChange.dispose() + this._onThreadNotify.dispose() + } + + public waitContinue(): Promise { + if (this._isRunning) { + return Promise.resolve() + } + + if (!this.awaitingContinue) { + this.awaitingContinue = new DeferredPromise() + const [to, cancel] = sleep(6000) + + this.awaitingContinue.p.finally(() => { + cancel() + delete this.awaitingContinue + }) + + to.then(() => { + (this.awaitingContinue || new DeferredPromise()).error(new Error('cannot continue program in 5s')) + }).catch(() => undefined) + } + + return this.awaitingContinue.p + } + + public waitInterrupt(): Promise { + if (!this._isRunning) { + return Promise.resolve() + } + + if (!this.awaitingInterrupt) { + this.awaitingInterrupt = new DeferredPromise() + const [to, cancel] = sleep(6000) + + this.awaitingInterrupt.p.finally(() => { + cancel() + delete this.awaitingInterrupt + }) + + to.then(() => { + (this.awaitingInterrupt || new DeferredPromise()).error(new Error('cannot interrupt program in 5s')) + }).catch(() => undefined) + } + + return this.awaitingInterrupt.p + } + + public async execInterrupt() { + return Promise.race([ + this.command('exec-interrupt').then(() => { + return timeout(4000)[0] + }), + this.waitInterrupt(), + ]).catch((e) => { + if (isCancel(e)) { + throw new Error('cannot x after 4s') + } else { + throw e + } + }) + } + + public async execContinue() { + return Promise.race([ + this.command('exec-continue').then(() => { + return timeout(4000)[0] + }), + this.waitContinue(), + ]).catch((e) => { + if (isCancel(e)) { + throw new Error('cannot x after 4s') + } else { + throw e + } + }) + } + + commandEnsure(command: string, ...args: string[]) { + const dfd = new DeferredPromise() + + this.command(command, ...args) + .progress((n) => dfd.notify(n)) + .catch((err) => { + if (isCommandIssueWhenRunning(err)) { + this.logger.info('retry after interrupt...') + + dfd.notify(err.node) + + this._onTargetRunStateChange.pause() + return this.execInterrupt().then((result) => { + const p = this.command(command, ...args) + p.progress((n) => dfd.notify(n)) + return p.finally(() => { + return this.execContinue() + }) + }, (err) => { + err.message += ' (when running command ' + command + ')' + throw err + }).finally(() => { + this._onTargetRunStateChange.resume() + }) + } else { + this.logger.info('error not cause by busy, no retry...') + throw err + } + }) + .then((data) => { + dfd.complete(data) + }, (err) => { + dfd.error(err) + }) + + return dfd.p + } + + command(command: string, ...args: string[]) { + const ret = this.controller.send([command, ...args].join(' ')) + + this.logger.info(`send command: ${ret.token} - ${command}`) + + ret.promise.then(() => { + this.logger.info(`command return: ${command}`) + }, (err) => { + this.logger.error(`command return: ${command} (with error)`) + }) + + return ret.promise + } + + async commandSequence(commandsArgs: [string, ...string[]][]): Promise { + const promise = commandsArgs.map(async ([command, ...args]) => { + return await this.command(command, ...args) + }) + const rets = await Promise.all(promise) + return rets[rets.length - 1] + } + + cliCommand(expression: string, threadId: number = 0, frameLevel: number = 0) { + const params: never[] = [] + const tParams = [] + if (threadId !== 0) { + tParams.push('--thread', threadId, '--frame', frameLevel) + } + const args: any[] = [...tParams, 'console', JSON.stringify(expression), ...params] + this.logger.info(`user command: interpreter-exec ${args.join(' ')}`) + return this.command('interpreter-exec', ...args) + } + + private handleMI(node: OneOfMiNodeType): boolean { + switch (node.type) { + case MiOutputType.asyncExec: + return this.handleExec(node) + case MiOutputType.asyncStatus: + return this.handleStatus(node) || false + case MiOutputType.asyncNotify: + return this.handleNotify(node) + case MiOutputType.result: + this.logger.debug('mi2 result message:', JSON.stringify(node, null, 2)) + return true + default: + return false + } + } + + private handleNotify(node: IAsyncNode) { + if (node.className === 'thread-created') { + this._onThreadNotify.fire({ type: ThreadNotify.Created, id: node.result('id') }) + } else if (node.className === 'thread-exited') { + this._onThreadNotify.fire({ type: ThreadNotify.Exited, id: node.result('id') }) + } else if (node.className === 'thread-group-added') { + this._onThreadNotify.fire({ type: ThreadNotify.GroupAdd, id: node.result('id') }) + } else if (node.className === 'thread-group-started') { + this._onThreadNotify.fire({ type: ThreadNotify.GroupStarted, id: node.result('id') }) + } else if (node.className === 'thread-group-exited') { + this._onThreadNotify.fire({ type: ThreadNotify.GroupExited, id: node.result('id') }) + } else if (node.className === 'breakpoint-modified') { + this.logger.warning(`// TODO notify: ${node.className}`) + } else if (node.className === 'memory-changed') { + this.logger.warning(`// TODO notify: ${node.className}`) + } else { + this.logger.warning(`missing notify: ${node.className}`) + return false + } + return true + } + + private handleStatus(node: IAsyncNode) { + if (node.isUnhandled() && node.className === 'download') { + this.logger.writeln('unhandled download: ' + JSON.stringify(node)) + return true + } + } + + private handleExec(node: IAsyncNode) { + this.logger.warning('MI2Node: ', node) + const event: IRunStateEvent = { + allThreads: node.result('stopped-threads') === 'all', + threadId: parseInt(node.result('thread-id')), + } as IRunStateEvent + if (node.className === 'stopped') { + event.running = false + const reason = node.result('reason') + + if (reason === undefined) { + event.reason = StopReason.UnknownReason + if (this.firstTimeStop) { + event.reason = StopReason.Startup + } + } else if (reason === 'breakpoint-hit') { + event.reason = StopReason.Breakpoint + } else if (reason === 'end-stepping-range') { + event.reason = StopReason.StepComplete + } else if (reason === 'function-finished') { + event.reason = StopReason.StepComplete + } else if (reason === 'signal-received') { + event.reason = StopReason.SignalStop + } else if (reason === 'exited-normally') { // this never run, we are running on chip + this.logger.error('Program exited normally') + setImmediate(() => {this.dispose()}) + } else if (reason === 'exited') { // this never run, we are running on chip + this.logger.error('Program exited with code ' + node.result('exit-code')) + setImmediate(() => {this.dispose()}) + } else { + this.logger.error('Not implemented stop reason (assuming exception): ' + reason) + return false + } + + event.reasonString = `${reason} @ ${node.result('frame.addr')} (${node.result('frame.func')})` + this.firstTimeStop = false + } else if (node.className === 'running') { + event.running = true + event.reason = StopReason.UnknownReason + } else { + return false + } + this.logger.debug(`exec event dump: ${dumpJson(node)}`) + + const stateChanged = this._isRunning !== event.running + this._isRunning = !!event.running + + if (event.running) { + if (this.awaitingInterrupt) { + this.awaitingInterrupt.error(new Error('program is not interrupt, but continue.')) + } + if (this.awaitingContinue) { + this.awaitingContinue.complete() + } + } else { + if (this.awaitingInterrupt) { + this.awaitingInterrupt.complete() + } + if (this.awaitingContinue) { + this.awaitingContinue.error(new Error('program is not interrupt, but continue.')) + } + } + + this.logger.writeln(`exec change [fire=${stateChanged}]: ${node.className}: ${StopReason[event.reason]} = ${event.reasonString}`) + event.realChange = stateChanged + this._onTargetRunStateChange.fire(event) + return true + } +} \ No newline at end of file diff --git a/src/debug/common/mi2/mi2CommandController.ts b/src/debug/common/mi2/mi2CommandController.ts new file mode 100644 index 0000000..45f6c73 --- /dev/null +++ b/src/debug/common/mi2/mi2CommandController.ts @@ -0,0 +1,157 @@ +import { ErrorMi2 } from '../../backend/lib/handleMethodPromise' +import { DeferredPromise, ProgressPromise } from '../deferredPromise' +import { Emitter } from '../event' +import { autoIncrease } from '../guid' +import { dumpJson } from '../library/strings' +import { IAsyncNode, IResultNode, OneOfMiNodeType } from './mi2Node' +import { isMi2Output, MiOutputType, parseMI } from './mi2Parser' +import { IMyLogger } from '@utils/baseLogger' +import split2 = require('split2') + +export interface IHandlerData extends IHandlerPublicData { + deferred: DeferredPromise + token: number + command: string +} + +export interface ISimpleOutput { + error: boolean + message: string +} + +export interface IHandlerPublicData { + promise: ProgressPromise + token: number + command: string +} + +const isPrompt = /^\s*\(gdb\)\s*$/ + +/** + * Mi2协议层 + * 接近原始MI2协议,输入指令字符串,运行,然后返回MI2结构体 + * 无自动应答操作 + */ +export class Mi2CommandController { + private readonly handlers = new Map() + // private readonly results = new WeakMap() + private readonly currentToken = autoIncrease() + + private readonly _onSimpleLine = new Emitter() + public readonly onSimpleLine = this._onSimpleLine.event + + private readonly _onReceiveMi2 = new Emitter() + public readonly onReceiveMi2 = this._onReceiveMi2.event + + constructor( + private readonly input: NodeJS.WritableStream, + private readonly output: NodeJS.ReadableStream, + private readonly logger: IMyLogger, + ) { + output.pipe(split2()).on('data', (line) => this.parseLine(line)) + } + + dispose() { + this._onSimpleLine.dispose() + this._onReceiveMi2.dispose() + } + + send(command: string): IHandlerPublicData { + const token = this.currentToken.next() + const deferred = new DeferredPromise() + + const ret: IHandlerData = { + token, + deferred, + promise: deferred.p, + command: command, + } + + deferred.p.finally(() => { + this.handlers.delete(token) + }) + + setImmediate(() => { + this.input.write(`${token}-${command}\n`) + }) + + this.handlers.set(token, ret) + return ret + } + + private parseLine(line: string) { + if (!isMi2Output.test(line)) { + if (isPrompt.test(line)) { + return + } + + this._onSimpleLine.fire({ + error: false, + message: line + '\n', + }) + return + } + + const node = parseMI(line) + node.handled = !!this.recv(line, node) + + if (node.isUnhandled()) { + this._onReceiveMi2.fire(node) + } + + if (node.isUnhandled()) { + this._onSimpleLine.fire({ + error: true, + message: 'Unhandled GDB output: ' + line + dumpJson(node) + '\n', + }) + } + } + + private recv(line: string, node: OneOfMiNodeType) { + switch (node.type) { + case MiOutputType.streamConsole: + this._onSimpleLine.fire({ + error: true, + message: node.content.replace(/\s+$/, '') + '\n', + }) + return true + case MiOutputType.streamLog: + this._onSimpleLine.fire({ + error: false, + message: node.content.replace(/\s+$/, '') + '\n', + }) + return true + case MiOutputType.streamTarget: + return false + } + + const item = this.handlers.get(node.token) + + if (node.type === MiOutputType.result) { + if (!item) { + throw new Error(`command ${node.token} return, but not exists: ${line}`) + } + if (node.className === 'error') { + const msg = node.result('msg') + item.deferred.error(new ErrorMi2(node, msg)) + return true + } else if (node.className) { + item.deferred.complete(node) + return true + } else { + return false + } + } + + if (!item) { + return false + } + + if (node.type === MiOutputType.asyncStatus) { + if (node.className === 'download') { + item.deferred.notify(node) + return true + } + } + } +} \ No newline at end of file diff --git a/src/debug/common/mi2/mi2Node.ts b/src/debug/common/mi2/mi2Node.ts new file mode 100644 index 0000000..edcc35e --- /dev/null +++ b/src/debug/common/mi2/mi2Node.ts @@ -0,0 +1,118 @@ +import { inspect } from 'util' +import { objectPath } from '../library/objectPath' +import { MiOutputType } from './mi2Parser' + +export interface IMapLike { + [varName: string]: IMapLike | ReadonlyArray | string +} + +interface WithData { + readonly data: IMapLike + result(path: string): T +} + +interface WithToken { + readonly token: number +} + +export type OneOfMiNodeType = IResultNode | IAsyncNode | IStreamNode + +export interface IResultNode extends WithData, WithToken, MINode { + readonly type: MiOutputType.result + + readonly className: 'done' | 'running' | 'connected' | 'error' | 'exit' +} + +export interface IAsyncNode extends WithData, WithToken, MINode { + readonly type: MiOutputType.asyncExec | MiOutputType.asyncStatus | MiOutputType.asyncNotify + + readonly className: string +} + +export interface IStreamNode extends MINode { + readonly type: MiOutputType.streamConsole | MiOutputType.streamTarget | MiOutputType.streamLog + readonly content: string +} + +export interface MINode { + rawLine: string + isUnhandled(): boolean + handled: boolean +} + +abstract class MIBaseImpl implements MINode { + private _handled: boolean = false + + protected constructor( + public readonly rawLine: string, + public readonly token: number, + public readonly data: any, + ) { + } + + [inspect.custom](depth: any, opts: { colors: any }) { + const obj = { + ...JSON.parse(JSON.stringify(this)), + } + delete obj.rawLine + + return '[' + this.constructor.name + ' ' + MiOutputType[(this as any).type] + '] ' + + (opts.colors ? '\x1B[38514m' : '') + this.rawLine + (opts.colors ? '\x1B[0m' : '') + ' ' + + JSON.stringify(obj, null, 4) + } + + isUnhandled() { + return !this._handled + } + + get handled() { + return this._handled + } + + set handled(v: boolean) { + if (v && !this._handled) { + this._handled = true + } + } + + result(path: string): T { + if (!this.data) { + throw new Error() + } + return objectPath(this.data as any, path) + } +} + +export class MIResultNodeImpl extends MIBaseImpl implements IResultNode { + constructor( + rawLine: string, + public readonly type: MiOutputType.result, + token: number, + readonly className: 'done' | 'running' | 'connected' | 'error' | 'exit', + data: any, + ) { + super(rawLine, token, data) + } +} + +export class MIAsyncNodeImpl extends MIBaseImpl implements IAsyncNode { + constructor( + rawLine: string, + public readonly type: MiOutputType.asyncExec | MiOutputType.asyncStatus | MiOutputType.asyncNotify, + token: number, + readonly className: string, + data: any, + ) { + super(rawLine, token, data) + } +} + +export class MIStreamNodeImpl extends MIBaseImpl implements IStreamNode { + constructor( + rawLine: string, + public readonly type: IStreamNode['type'], + public readonly content: any, + ) { + super(rawLine, 0, undefined) + } +} diff --git a/src/debug/common/mi2/mi2Parser.ts b/src/debug/common/mi2/mi2Parser.ts new file mode 100644 index 0000000..d00ebd7 --- /dev/null +++ b/src/debug/common/mi2/mi2Parser.ts @@ -0,0 +1,291 @@ +/** + * output ==> + * ( + * exec-async-output = [ token ] "*" ("stopped" | others) ( "," variable "=" (const | tuple | list) )* \n + * status-async-output = [ token ] "+" ("stopped" | others) ( "," variable "=" (const | tuple | list) )* \n + * notify-async-output = [ token ] "=" ("stopped" | others) ( "," variable "=" (const | tuple | list) )* \n + * console-stream-output = "~" c-string \n + * target-stream-output = "@" c-string \n + * log-stream-output = "&" c-string \n + * )* + * [ + * [ token ] "^" ("done" | "running" | "connected" | "error" | "exit") ( "," variable "=" (const | tuple | list) )* \n + * ] + * "(gdb)" \n + */ +import { escapeCString } from './cString' +import { MIAsyncNodeImpl, MIResultNodeImpl, MIStreamNodeImpl, OneOfMiNodeType } from './mi2Node' +import { Mi2SyntaxError } from './syntaxError' + +export const isMi2Output = /^(?:\d*|undefined)[*+=]|[~@&^]/ + +const tokenRegex = /^\d+/ +const asyncRecordRegex = /^(\d*|undefined)([*+=])/ +const streamRecordRegex = /^([~@&])/ +const resultRecordRegex = /^(\d*)\^(done|running|connected|error|exit)/ +const variableRegex = /^[a-zA-Z_\-][a-zA-Z0-9_\-]*/ + +export enum MiOutputType { + asyncExec, + asyncStatus, + asyncNotify, + streamConsole, + streamTarget, + streamLog, + result, +} + +function escapeRegExp(string: { replace: (arg0: RegExp, arg1: string) => void }) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string +} + +type CallbackType = (() => any) | ((required: boolean) => any) + +function popAll(...cbList: CallbackType[]): any[] { + let ret = [] + + function firstOkCb() { + for (const cb of cbList) { + const ret = cb(false) + if (ret) { + return ret + } + } + } + + for (let value = firstOkCb(); value; value = firstOkCb()) { + ret.push(value) + } + return ret +} + +export function parseMI(output: string): OneOfMiNodeType { + output = output.trim() + const remOutput = output + + function isEnding() { + return output.length === 0 + } + + function findMatch(wantList: string[] | string) { + const subs = Array.isArray(wantList) ? wantList : [wantList] + let match = 0 + subs.every((sub) => { + if (sub === output.substr(0, sub.length)) { + match = sub.length + return false + } else { + return true + } + }) + return match + } + + function assertIs(wantList: string[] | string, extraMessage?: string) { + const match = findMatch(wantList) + if (!match) { + throw new Mi2SyntaxError(`missing "${wantList}" ${extraMessage || ''}`, remOutput, output) + } + } + + function readReg(reg: RegExp, required = false) { + const m = reg.exec(output) + if (!m) { + if (required) { + throw new Mi2SyntaxError(`missing "${reg}"`, remOutput, output) + } + return undefined + } + output = output.substr(m[0].length) + return m + } + + function read(length: number) { + if (!length) { + return undefined + } + const ret = output.substr(0, length) + output = output.substr(length) + return ret + } + + function cut(wantList: string[] | string, required: boolean = false) { + if (required) { + assertIs(wantList) + } + return read(findMatch(wantList)) + } + + function parseCString() { + // console.log('parseCString:', output) + assertIs('"') + let stringEnd = 1 + let inString = true + let remaining = output.substr(stringEnd) + let escaping = false + while (inString && remaining.length) { + if (escaping) { + escaping = false + } else if (remaining[0] == '\\') { + escaping = true + } else if (remaining[0] == '"') { + inString = false + } + + remaining = remaining.substr(1) + stringEnd++ + } + + if (inString) { + throw new Mi2SyntaxError('missing string ending', remOutput, output) + } + + return escapeCString(read(stringEnd) || '') + } + + function parseTupleOrList(required = false) { + // console.log('parseTupleOrList:', output) + const first = cut(['{', '['], required) + if (!first) { + return undefined + } + const isArray = first === '[' + if (isArray) { + const ret = popAll(parseValue, parseResult, parseCommaAny) + cut(']', true) + return ret + } else { + const objectItems = popAll(parseResult, parseCommaAny) + const ret = Object.assign({}, ...objectItems) + // console.log(ret) + cut('}', true) + return ret + } + } + + function parseValue(required = false) { // parse xxxx= + // console.log('parseValue:', output) + if (findMatch('"')) { + return parseCString() + } else if (findMatch(['{', '['])) { + return parseTupleOrList(required) + } else if (required) { + throw new Mi2SyntaxError('require a value', remOutput, output) + } else { + return undefined + } + } + + function parseResult(required = false) { // parse + const variableMatch = variableRegex.exec(output) + if (!variableMatch) { + if (required) { + throw new Mi2SyntaxError('require result', remOutput, output) + } + return undefined + } + const variableName = variableMatch[0] + read(variableName.length) + cut('=', true) + const mainValue = parseValue(true) + if (mainValue && typeof mainValue === 'object' && !Array.isArray(mainValue)) { + while (findMatch(',{')) { + const child = parseCommaTupleOrList(true) + if (mainValue.MI2ChildValues) { + if (Array.isArray(mainValue.MI2ChildValues)) { + mainValue.MI2ChildValues.push(child) + } else { + mainValue.MI2ChildValues = [mainValue.MI2ChildValues, child] + } + } else { + mainValue.MI2ChildValues = child + } + } + } + return { [variableName]: mainValue } + } + + function parseCommaTupleOrList(required = false) { // parse <,???> + if (!cut(',', required)) { + return undefined + } + const value = parseTupleOrList() + if (required && value === undefined) { + throw new Mi2SyntaxError('require object or array ', remOutput, output) + } + return value + } + + function parseCommaAny(required = false) { // parse <,???> + if (!cut(',', required)) { + return undefined + } + const value = parseResult() || parseTupleOrList() + if (required && value === undefined) { + throw new Mi2SyntaxError('require result or object or array ', remOutput, output) + } + return value + } + + function parseCommaResult(required = false) { // parse <,xxxx=???> + if (!cut(',', required)) { + return undefined + } + const value = parseResult() + if (required && value === undefined) { + throw new Mi2SyntaxError('require result ', remOutput, output) + } + return value + } + + const tokenMatch = readReg(/^\d+/) + const token = tokenMatch ? parseInt(tokenMatch[0]) : NaN + const type = cut(['*', '+', '=', '^', '~', '@', '&'], true) + + function handleRecord() { + const className = (readReg(variableRegex, true) || [])[0] + if (isEnding()) { + return { className, value: undefined } + } + cut(',', true) + let value = parseTupleOrList() + if (value) { + if (!isEnding()) { + throw new Mi2SyntaxError('expect eol ', remOutput, output) + } + } else { + const valueArr = popAll(parseResult, parseCommaResult) + if (!isEnding()) { + throw new Mi2SyntaxError('expect eol ', remOutput, output) + } + value = Object.assign({}, ...valueArr) + } + return { className, value } + } + + if (type === '*') { + const { className, value } = handleRecord() + return new MIAsyncNodeImpl(remOutput, MiOutputType.asyncExec, token, className, value) + } else if (type === '+') { + const { className, value } = handleRecord() + return new MIAsyncNodeImpl(remOutput, MiOutputType.asyncStatus, token, className, value) + } else if (type === '=') { + const { className, value } = handleRecord() + return new MIAsyncNodeImpl(remOutput, MiOutputType.asyncNotify, token, className, value) + } else if (type === '^') { + const { className, value } = handleRecord() + return new MIResultNodeImpl(remOutput, MiOutputType.result, token, className as any, value) + } else if (type === '~') { + const content = parseCString() + return new MIStreamNodeImpl(remOutput, MiOutputType.streamConsole, content) + } else if (type === '@') { + const content = parseCString() + return new MIStreamNodeImpl(remOutput, MiOutputType.streamTarget, content) + } else if (type === '&') { + const content = parseCString() + return new MIStreamNodeImpl(remOutput, MiOutputType.streamLog, content) + } else { + const content = parseCString() + return new MIStreamNodeImpl(remOutput, MiOutputType.streamLog, content) + } +} diff --git a/src/debug/common/mi2/pause.ts b/src/debug/common/mi2/pause.ts new file mode 100644 index 0000000..f2c1dd9 --- /dev/null +++ b/src/debug/common/mi2/pause.ts @@ -0,0 +1,47 @@ +import { StoppedEvent } from 'vscode-debugadapter' +import { DebugProtocol } from 'vscode-debugprotocol' + + +export function createStopEvent(reason: StopReason, threadId?: number, description?: string) { + const ev: DebugProtocol.StoppedEvent = new StoppedEvent(stopReasonString(StopReason.Pausing)) + const b = ev.body + if (threadId) { + b.threadId = threadId + } else { + b.threadId = 1 + b.allThreadsStopped = true + } + if (description) { + b.description = description + } + return ev +} + +export function stopReasonString(reason: StopReason) { + switch (reason) { + case StopReason.Breakpoint: + return 'breakpoint' + case StopReason.StepComplete: + return 'step' + case StopReason.SignalStop: + return 'pause' + case StopReason.UserCause: + return 'user request' + case StopReason.Startup: + return 'entry' + case StopReason.Pausing: + return 'pause' + default: + return 'unknown' + } +} + +export enum StopReason { + UnknownReason, + Breakpoint, + StepComplete, + SignalStop, + Startup, + UserCause, + Pausing, +} \ No newline at end of file diff --git a/src/debug/common/mi2/syntaxError.ts b/src/debug/common/mi2/syntaxError.ts new file mode 100644 index 0000000..d4c3d46 --- /dev/null +++ b/src/debug/common/mi2/syntaxError.ts @@ -0,0 +1,9 @@ +const stackModify = / \(.+\/mi2Parser\.[tj]s:\d+:\d+\)/g + +export class Mi2SyntaxError extends SyntaxError { + constructor(msg: string, fullOutput: string, output: string) { + const o = fullOutput.substr(0, fullOutput.length - output.length) + '🔥' + output + super(`${msg}\n of: ${o}`) + this.stack = (this.stack || '').replace(stackModify, '') + } +} diff --git a/src/debug/common/mi2/types.convert.ts b/src/debug/common/mi2/types.convert.ts new file mode 100644 index 0000000..1543793 --- /dev/null +++ b/src/debug/common/mi2/types.convert.ts @@ -0,0 +1,20 @@ +import { DebugProtocol } from 'vscode-debugprotocol' +import { BreakpointType, MyBreakpoint } from './types' + +export function toProtocolBreakpoint(breakpoint: MyBreakpoint): DebugProtocol.Breakpoint { + if (breakpoint.tried && (breakpoint.gdbBreakNum || 0) > 0) { + return { + id: breakpoint.gdbBreakNum, + verified: true, + source: { + path: breakpoint.type === BreakpointType.Line ? breakpoint.file : undefined, + }, + line: breakpoint.type === BreakpointType.Line ? breakpoint.line : undefined, + } + } else { + return { + verified: false, + message: breakpoint.errorMessage, + } + } +} \ No newline at end of file diff --git a/src/debug/common/mi2/types.ts b/src/debug/common/mi2/types.ts new file mode 100644 index 0000000..601bf3c --- /dev/null +++ b/src/debug/common/mi2/types.ts @@ -0,0 +1,50 @@ +export enum BreakpointType { + Function, + Line, +} + +interface MyBreakpointBase { + gdbBreakNum?: number + type: BreakpointType + condition: string + // countCondition: string + tried?: boolean + errorMessage?: string + addr?: string +} + +export interface MyBreakpointLine extends MyBreakpointBase { + type: BreakpointType.Line + file: string + line: number + logMessage: string +} + +export interface MyBreakpointFunc extends MyBreakpointBase { + type: BreakpointType.Function + name: string +} + +export type MyBreakpoint = MyBreakpointLine | MyBreakpointFunc + +export interface IMyThread { + id: number + targetId: string + name?: string +} + +export interface IMyStack { + level: number + address: string + function: string + fileName: string + file: string + line: number +} + +export interface IMyVariable { + name: string + valueStr: string + type: string + raw?: any +} diff --git a/src/debug/frontend/lib/backendLogReceiver.ts b/src/debug/frontend/lib/backendLogReceiver.ts new file mode 100644 index 0000000..9585a60 --- /dev/null +++ b/src/debug/frontend/lib/backendLogReceiver.ts @@ -0,0 +1,65 @@ +import { createWriteStream, WriteStream } from 'fs' +import * as vscode from 'vscode' +import { IMyLogger, LogLevel } from '@utils/baseLogger' +import { ICustomEvent, ILogEventBody } from '../../common/eventProtocol' +import { FrontendChannelLogger } from '@utils/extensionLogger' + +export class BackendLogReceiver { + public readonly logger: FrontendChannelLogger + private handled!: vscode.Disposable + + constructor() { + this.logger = new FrontendChannelLogger('B', 'kendryte.gdb') + this.start() + } + + dispose() { + this.handled.dispose() + } + + private start() { + this.handled = vscode.debug.onDidReceiveDebugSessionCustomEvent((e: any) => { + const customEvent: ICustomEvent = e.body + if (!customEvent.event || !customEvent.type) { + return + } + switch (customEvent.type) { + case 'nl': + this.logger.writeln('') + break + case 'log': + const { level, message, args } = customEvent.event as ILogEventBody + doLog(this.logger, level, message, args || []) + break + case 'clear-log': + this.logger.clear() + break + default: + this.logger.warning('Unknown event type: ' + customEvent.type + ': ' + JSON.stringify(customEvent.event)) + break + } + }) + } +} + +function doLog(logger: IMyLogger, level: LogLevel, message: string, args: any[]): void { + switch (level) { + case LogLevel.Trace: + return logger.trace(message, ...args) + case LogLevel.Debug: + return logger.debug(message, ...args) + case LogLevel.Info: + return logger.info(message, ...args) + case LogLevel.Warning: + return logger.warning(message, ...args) + case LogLevel.Error: + return logger.error(message, ...args) + case LogLevel.Critical: + return logger.critical(message, ...args) + case LogLevel.Off: + return logger.writeln(message, ...args) + default: + logger.warning('Unknown log message level: %s', level) + return logger.writeln(message, ...args) + } +} diff --git a/src/debug/messages.ts b/src/debug/messages.ts new file mode 100644 index 0000000..d9fc0ca --- /dev/null +++ b/src/debug/messages.ts @@ -0,0 +1,9 @@ +import * as nls from 'vscode-nls' + +const localize = nls.config({ messageFormat: nls.MessageFormat.file })() + +export const MESSAGE_LOADING_PROGRAM = localize('loadingProgram', 'loading program:') + +export function MESSAGE_DOWNLOAD_SPEED(completeStr: string, speedLevel: string, kbpsStr: string) { + return localize('kendryte.download.message', 'Complete: {0} {1} Speed@ {2}', completeStr, speedLevel, kbpsStr) +} \ No newline at end of file diff --git a/src/debugadapter.ts b/src/debugadapter.ts new file mode 100644 index 0000000..162d843 --- /dev/null +++ b/src/debugadapter.ts @@ -0,0 +1,10 @@ +import { DebugSession } from 'vscode-debugadapter' +import { KendryteDebugger } from '@debug/backend/kendryteDebugger' + +require('source-map-support/register') + +console.error('\n[kendryte debug] debugger loader.') +console.error('\n * ' + process.argv.join('\n * ')) +process.title = 'gdb-session' + +DebugSession.run(KendryteDebugger) \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts new file mode 100644 index 0000000..effe05d --- /dev/null +++ b/src/extension.ts @@ -0,0 +1,310 @@ +import * as path from 'path' +import * as vscode from 'vscode' +import { initialization } from './initialization' +import * as command from '@command/index' +import { DevPackages } from './views/DevPackages' +import { Devices, OpenocdService, SerialPortService } from '@service/index' + +import * as fs from 'fs' +import * as net from 'net' +import * as os from 'os' +import { CancellationToken, DebugConfiguration, ProviderResult, WorkspaceFolder } from 'vscode' +import { IMyLogger } from '@utils/baseLogger' +import { BackendLogReceiver } from '@debug/frontend/lib/backendLogReceiver' +import { disposeChannel, FrontendChannelLogger } from '@utils/extensionLogger' + + +export const activate = async (context: vscode.ExtensionContext) => { + // Initialzate + await initialization(context) + + // Create dependencies tree view + const dependenciesProvider = new DevPackages(vscode.workspace.rootPath) + vscode.window.registerTreeDataProvider('packageDependencies', dependenciesProvider) + vscode.commands.registerCommand('packageDependencies.refresh', () => dependenciesProvider.refresh()) + + // Create log channel + const openocdLogger = new FrontendChannelLogger('Openocd', 'openocd') + const uploadLogger = new FrontendChannelLogger('Upload', 'upload') + const serialportLogger = new FrontendChannelLogger('Serialport', 'serialport') + + // Status bar items + const home = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 100) + home.command = 'packageDependencies.createWebview' + home.text = '$(home) Kendryte Homepage' + home.show() + context.subscriptions.push(home) + + const build = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left) + build.command = 'extension.build' + build.tooltip = 'Build' + build.text = '$(tools)' + build.show() + context.subscriptions.push(build) + + const upload = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left) + upload.command = 'extension.buildAndUpload' + upload.tooltip = 'Build and Upload' + upload.text = '$(cloud-download)' + upload.show() + context.subscriptions.push(upload) + + const debug = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left) + debug.command = 'extension.debug' + debug.tooltip = 'Debug' + debug.text = '$(bug)' + debug.show() + context.subscriptions.push(debug) + + const serialport = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left) + serialport.command = 'extension.openSerialPort' + serialport.tooltip = 'Open serial port output' + serialport.text = '$(terminal)' + serialport.show() + context.subscriptions.push(serialport) + const updateSerialPort = (open: boolean) => { + serialport.text = !open ? '$(terminal)' : '$(primitive-square)' + serialport.tooltip = !open ? 'Open serial port output' : 'Stop serial port' + } + + // SerialPort container service + const serialPortService = new SerialPortService(updateSerialPort, context.extensionPath) + + // Devices check service + const deviceServices = new Devices(serialPortService) + deviceServices.on('setdevice', device => { + updateDevice(device) + }) + + const device = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left) + device.command = 'extension.pickDevice' + device.tooltip = 'Pick a device' + device.text = deviceServices.device || 'No device picked.' + device.show() + context.subscriptions.push(device) + const updateDevice = (deviceName: string | undefined) => { + device.text = deviceName || 'No device picked.' + } + + // Register extension command + context.subscriptions.push(command.reinstallPackages(context)) + context.subscriptions.push(command.build(context)) + context.subscriptions.push(command.createHelloworld(context)) + context.subscriptions.push(command.buildAndUpload(context, deviceServices, uploadLogger, serialPortService)); + context.subscriptions.push(command.debug(context)) + context.subscriptions.push(command.pickDevice(deviceServices)) + context.subscriptions.push(command.configGenerate()) + context.subscriptions.push(command.cmakelistGenerate()) + context.subscriptions.push(command.createWebview(context)) + context.subscriptions.push(command.dependenciesDownload(context)) + context.subscriptions.push(command.addDependency(context, dependenciesProvider)) + context.subscriptions.push(command.deleteDependency(dependenciesProvider)) + context.subscriptions.push(command.openSerialPort(deviceServices, serialportLogger, serialPortService)) + + // Openocd service command + const openocdService = new OpenocdService(openocdLogger) + context.subscriptions.push(command.openocdStart(openocdService)) + context.subscriptions.push(command.openocdStop(openocdService)) + context.subscriptions.push(command.openocdRestart(openocdService)) + + // Debug Part + // The following part is copied from https://github.com/GongT/kendryte-ide-shell/blob/master/extensions.kendryte/kendryte-debug/src/frontend/extension.ts + context.subscriptions.push(vscode.workspace.registerTextDocumentContentProvider('debugmemory', new MemoryContentProvider())) + context.subscriptions.push(vscode.commands.registerCommand('kendryte-debug.examineMemoryLocation', examineMemory)) + context.subscriptions.push(vscode.commands.registerCommand('kendryte-debug.getFileNameNoExt', () => { + if (!vscode.window.activeTextEditor || !vscode.window.activeTextEditor.document || !vscode.window.activeTextEditor.document.fileName) { + vscode.window.showErrorMessage('No editor with valid file name active') + return + } + const fileName = vscode.window.activeTextEditor.document.fileName + const ext = path.extname(fileName) + return fileName.substr(0, fileName.length - ext.length) + })) + context.subscriptions.push(vscode.commands.registerCommand('kendryte-debug.getFileBasenameNoExt', () => { + if (!vscode.window.activeTextEditor || !vscode.window.activeTextEditor.document || !vscode.window.activeTextEditor.document.fileName) { + vscode.window.showErrorMessage('No editor with valid file name active') + return + } + const fileName = path.basename(vscode.window.activeTextEditor.document.fileName) + const ext = path.extname(fileName) + return fileName.substr(0, fileName.length - ext.length) + })) + context.subscriptions.push(new BackendLogReceiver()) + context.subscriptions.push({ + dispose: disposeChannel, + }) + + vscode.debug.registerDebugConfigurationProvider('kendryte', new Provider()) + + // Create config files and watch kendryte-package.json + const files = await vscode.workspace.findFiles('kendryte-package.json') + if (files.length > 0) { + await vscode.commands.executeCommand('extension.configGenerate') + const watcher = vscode.workspace.createFileSystemWatcher(files[0].path) + watcher.onDidChange(_ => { + vscode.commands.executeCommand('extension.configGenerate') + }) + } + vscode.commands.executeCommand('packageDependencies.createWebview') +} + +class Provider implements vscode.DebugConfigurationProvider { + private readonly logger: IMyLogger + + constructor() { + this.logger = new FrontendChannelLogger('Provider', 'kendryte.gdb') + } + + provideDebugConfigurations(folder: WorkspaceFolder | undefined, token?: CancellationToken): ProviderResult { + this.logger.info('createDebugAdapterDescriptor', arguments) + return [] + } + + createDebugAdapterDescriptor() { + this.logger.info('createDebugAdapterDescriptor', arguments) + debugger + } +} + +const memoryLocationRegex = /^0x[0-9a-f]+$/ + +function getMemoryRange(range: string) { + if (!range) { + return undefined + } + range = range.replace(/\s+/g, '').toLowerCase() + let index + if ((index = range.indexOf('+')) != -1) { + const from = range.substr(0, index) + let length = range.substr(index + 1) + if (!memoryLocationRegex.exec(from)) { + return undefined + } + if (memoryLocationRegex.exec(length)) { + length = parseInt(length.substr(2), 16).toString() + } + return 'from=' + encodeURIComponent(from) + '&length=' + encodeURIComponent(length) + } else if ((index = range.indexOf('-')) != -1) { + const from = range.substr(0, index) + const to = range.substr(index + 1) + if (!memoryLocationRegex.exec(from)) { + return undefined + } + if (!memoryLocationRegex.exec(to)) { + return undefined + } + return 'from=' + encodeURIComponent(from) + '&to=' + encodeURIComponent(to) + } else if (memoryLocationRegex.exec(range)) { + return 'at=' + encodeURIComponent(range) + } else { + return undefined + } +} + +function examineMemory() { + const socketlists = path.join(os.tmpdir(), 'kendryte-debug-sockets') + if (!fs.existsSync(socketlists)) { + if (process.platform == 'win32') { + return vscode.window.showErrorMessage('This command is not available on windows') + } else { + return vscode.window.showErrorMessage('No debugging sessions available') + } + } + fs.readdir(socketlists, (err, files) => { + if (err) { + if (process.platform == 'win32') { + return vscode.window.showErrorMessage('This command is not available on windows') + } else { + return vscode.window.showErrorMessage('No debugging sessions available') + } + } + const pickedFile = (file: string | undefined) => { + vscode.window.showInputBox({ + placeHolder: 'Memory Location or Range', + validateInput: (range: string) => getMemoryRange(range) === undefined ? 'Range must either be in format 0xF00-0xF01, 0xF100+32 or 0xABC154' : '', + }).then(range => { + vscode.commands.executeCommand('vscode.previewHtml', vscode.Uri.parse('debugmemory://' + file + '#' + getMemoryRange(range || ''))) + }) + } + if (files.length == 1) { + pickedFile(files[0]) + } else if (files.length > 0) { + vscode.window.showQuickPick(files, { placeHolder: 'Running debugging instance' }).then(file => pickedFile(file)) + } else if (process.platform == 'win32') { + return vscode.window.showErrorMessage('This command is not available on windows') + } else { + vscode.window.showErrorMessage('No debugging sessions available') + } + }) +} + +class MemoryContentProvider implements vscode.TextDocumentContentProvider { + provideTextDocumentContent(uri: vscode.Uri, token: vscode.CancellationToken): Thenable { + return new Promise((resolve, reject) => { + const conn = net.connect(path.join(os.tmpdir(), 'kendryte-debug-sockets', uri.authority)) + let from: number, to: number + let highlightAt = -1 + const splits = uri.fragment.split('&') + if (splits[0].split('=')[0] == 'at') { + const loc = parseInt(splits[0].split('=')[1].substr(2), 16) + highlightAt = 64 + from = Math.max(loc - 64, 0) + to = Math.max(loc + 768, 0) + } else if (splits[0].split('=')[0] == 'from') { + from = parseInt(splits[0].split('=')[1].substr(2), 16) + if (splits[1].split('=')[0] == 'to') { + to = parseInt(splits[1].split('=')[1].substr(2), 16) + } else if (splits[1].split('=')[0] == 'length') { + to = from + parseInt(splits[1].split('=')[1]) + } else { + return reject('Invalid Range') + } + } else { + return reject('Invalid Range') + } + if (to < from) { + return reject('Negative Range') + } + conn.write('examineMemory ' + JSON.stringify([from, to - from + 1])) + conn.once('data', data => { + let formattedCode = '' + const hexString = data.toString() + let x = 0 + let asciiLine = '' + let byteNo = 0 + for (let i = 0; i < hexString.length; i += 2) { + const digit = hexString.substr(i, 2) + const digitNum = parseInt(digit, 16) + if (digitNum >= 32 && digitNum <= 126) { + asciiLine += String.fromCharCode(digitNum) + } else { + asciiLine += '.' + } + if (highlightAt == byteNo) { + formattedCode += '' + digit + ' ' + } else { + formattedCode += digit + ' ' + } + if (++x > 16) { + formattedCode += asciiLine + '\n' + x = 0 + asciiLine = '' + } + byteNo++ + } + if (x > 0) { + for (let i = 0; i <= 16 - x; i++) { + formattedCode += ' ' + } + formattedCode += asciiLine + } + resolve('

Memory Range from 0x' + from.toString(16) + ' to 0x' + to.toString(16) + '

' + formattedCode + '
') + conn.destroy() + }) + }) + } +} + + +// this method is called when your extension is deactivated +export function deactivate() { } diff --git a/src/initialization.ts b/src/initialization.ts new file mode 100644 index 0000000..a67d048 --- /dev/null +++ b/src/initialization.ts @@ -0,0 +1,100 @@ +import * as vscode from 'vscode' +import * as fs from 'fs' +import Axios from 'axios' +import * as os from 'os' +import { setPackagePath, systemFilter, downloadPackage, unArchive, urlJoin } from '@utils/index' +import { PackageData, PlatformPackage, PackagesVersion, GlobalConfig } from './interface' +import { join } from 'path' +import * as chmodr from 'chmodr' +export const initialization = async (context: vscode.ExtensionContext) => { + const config = JSON.parse(fs.readFileSync(`${context.extensionPath}/config.json`, 'utf8')) + let packages: PackageData[] + try { + packages = await getRemotePackagesVersion(config) + } catch(e) { + vscode.window.showErrorMessage(e) + return + } + await installPackages(context, config, packages) +} + +const installPackages = async (context: vscode.ExtensionContext, config: GlobalConfig, packages: PackageData[]): Promise => { + // Get system information + let packagePath = setPackagePath() + + // Get local package version + let packagesVersions: PackagesVersion = context.globalState.get('k210Packages') || {} + + // Get package list + vscode.window.withProgress({ + location: vscode.ProgressLocation.Notification, + title: 'Downloading Packages.', + cancellable: false + }, async(progress) => { + progress.report({ increment: 0, message: 'Preparing' }) + + // Download packages + for (let packageData of packages) { + let url = '' + let remoteVersion: string = '' + + if (packageData.source) { + url = urlJoin(config.host, packageData.source) + remoteVersion = packageData.version + } else { + if ((!packageData.darwin && os.platform() === 'darwin') || (!packageData.linux && os.platform() === 'linux')) { + console.log(`No need to install ${packageData.projectName}`) + continue + } + const platform = systemFilter(packageData.win32, packageData.darwin, packageData.linux) + remoteVersion = packageData.version + url = platform ? urlJoin(config.host, platform.source) : '' + } + + // Progress report + const increceProgress = 1 / packages.length * 100 + progress.report({ increment: increceProgress, message: `Downloading ${packageData.projectName}` }) + + // Package install + packagesVersions = await downloadAndExtract(packageData.projectName, url, remoteVersion, packagePath, packagesVersions, context) + } + }) +} + +const getRemotePackagesVersion = (config: GlobalConfig): Promise> => { + return new Promise(async (resolve, reject) => { + try { + const res = await Axios({ + method: "get", + url: urlJoin(config.host, 'lib', 'list.json'), + responseType: "json" + }) + resolve(res.data) + } catch(e) { + reject(`Request remote package list error.${e.message}`) + } + }) +} + +const downloadAndExtract = (packageName: string, url: string, remoteVersion: string, packagePath: string, packagesVersions: PackagesVersion, context: vscode.ExtensionContext): Promise => { + return new Promise(async (resolve, reject) => { + // Check package update + if (packagesVersions[packageName] === remoteVersion) { + console.log(`Skip ${packageName}`) + resolve(packagesVersions) + return + } + + // Main + try { + await downloadPackage(packagePath, url, url.replace(/(.*\/)*([^.]+)/i,"$2")) // 正则获取url尾部的文件名 + await unArchive(join(packagePath, url.replace(/(.*\/)*([^.]+)/i,"$2")), packagePath) + chmodr(join(process.env['packagePath'] || '', packageName), 0o755, console.log) + packagesVersions[packageName] = remoteVersion + await context.globalState.update('k210Packages', packagesVersions) + } catch(e) { + vscode.window.showErrorMessage(`Download ${packageName} error.${e.message}`) + } + resolve(packagesVersions) + }) +} \ No newline at end of file diff --git a/src/interface/globalconfig.ts b/src/interface/globalconfig.ts new file mode 100644 index 0000000..d83642d --- /dev/null +++ b/src/interface/globalconfig.ts @@ -0,0 +1,6 @@ +export interface GlobalConfig { + cdn: string + third_party: string + package_version: string + host: string +} \ No newline at end of file diff --git a/src/interface/index.ts b/src/interface/index.ts new file mode 100644 index 0000000..160ae03 --- /dev/null +++ b/src/interface/index.ts @@ -0,0 +1,2 @@ +export * from './package' +export * from './globalconfig' \ No newline at end of file diff --git a/src/interface/package.ts b/src/interface/package.ts new file mode 100644 index 0000000..d979bf3 --- /dev/null +++ b/src/interface/package.ts @@ -0,0 +1,16 @@ +export interface PlatformPackage { + readonly source: string +} + +export interface PackageData { + readonly projectName: string + readonly win32: PlatformPackage + readonly darwin?: PlatformPackage + readonly linux?: PlatformPackage + readonly version: string + readonly source: string +} + +export interface PackagesVersion { + [key: string]: string +} \ No newline at end of file diff --git a/src/service/devices.ts b/src/service/devices.ts new file mode 100644 index 0000000..efc7039 --- /dev/null +++ b/src/service/devices.ts @@ -0,0 +1,53 @@ +import * as vscode from 'vscode' +import { list, PortInfo } from 'serialport' +import { EventEmitter } from 'events' +import { SerialPortService } from './serialport' + +export class Devices extends EventEmitter { + private currentDevice: string | undefined + private serialPort: SerialPortService + + constructor(sp: SerialPortService) { + super() + this.serialPort = sp + } + + public async getDiveceList(): Promise { + return new Promise(async resolve => { + resolve(await list()) + }) + } + + get device(): string | undefined { + return this.currentDevice + } + + // Pick a device + public async setDevice(device?: string): Promise { + return new Promise(async (resolve, reject) => { + this.currentDevice = device + if (this.currentDevice) { + this.serialPort.createSerialPortConnect(this.currentDevice) + .then(() => { + this.serialPort.on('close', err => { + if (err && err.disconnected) { + this.serialPort.disposeSerialPort() + vscode.window.showInformationMessage(`${this.currentDevice} disconnected.`) + this.setDevice() + } + this.serialPort.removeAllListeners() + }) + this.emit('setdevice', this.currentDevice) + resolve() + }) + .catch(err => { + vscode.window.showErrorMessage(err.message) + resolve() + }) + } else { + this.emit('setdevice', this.currentDevice) + resolve() + } + }) + } +} \ No newline at end of file diff --git a/src/service/index.ts b/src/service/index.ts new file mode 100644 index 0000000..e248e5b --- /dev/null +++ b/src/service/index.ts @@ -0,0 +1,3 @@ +export * from '@service/devices' +export * from '@service/openocd' +export * from '@service/serialport' \ No newline at end of file diff --git a/src/service/openocd/index.ts b/src/service/openocd/index.ts new file mode 100644 index 0000000..a7724e5 --- /dev/null +++ b/src/service/openocd/index.ts @@ -0,0 +1,55 @@ +import * as vscode from 'vscode' +import { FrontendChannelLogger, systemFilter } from '@utils/index' +import { spawn } from 'child_process' +import { Process } from '@service/openocd/openocdprocess' + +export class OpenocdService { + private process?: Process + readonly logger: FrontendChannelLogger + constructor( + logger: FrontendChannelLogger + ) { + this.logger = logger + } + + // Create openocd process + private createOpenocdProcess = () => { + this.process = new Process(this.logger, this.clearProcess) + } + + public start = (): void => { + if (this.process) { + this.logger.info('Openocd Service is running') + return + } + this.createOpenocdProcess() + } + public stop = (): void => { + if (!this.process) { + this.logger.info('Openocd service not found') + return + } + this.process.kill() + this.logger.info('Openocd is closed') + delete this.process + } + public restart = (): void => { + /* + Why use killall command? + To prevent openocd service error and process.kill doesn't work. + */ + const killCommand = systemFilter('taskkill', 'killall', 'killall') + const killArgs = systemFilter(['/IM', '"openocd.exe"', '/F'], ['SIGKILL', 'openocd'], ['SIGKILL', 'openocd']) + const killProcess = spawn(killCommand, killArgs) + killProcess.stdout.on('data', data => this.logger.info(data)) + killProcess.stderr.on('data', data => this.logger.error(data)) + killProcess.on('close', _ => { + delete this.process + vscode.commands.executeCommand('extension.openocd.start') + }) + } + + clearProcess = () => { + delete this.process + } +} \ No newline at end of file diff --git a/src/service/openocd/openocdprocess.ts b/src/service/openocd/openocdprocess.ts new file mode 100644 index 0000000..703e50a --- /dev/null +++ b/src/service/openocd/openocdprocess.ts @@ -0,0 +1,65 @@ +import * as vscode from 'vscode' +import { spawn } from 'child_process' +import { FrontendChannelLogger, throttleOffset, systemFilter } from '@utils/index' +import { join } from 'path' + +export class Process { + private readonly pid: number + private readonly logger: FrontendChannelLogger + private readonly clearFunc: () => void + + constructor( + logger: FrontendChannelLogger, + clearFunc: () => void + ) { + this.logger = logger + this.clearFunc = clearFunc + this.pid = this.start() + } + private start(): number { + const args = ['-f', `${vscode.workspace.rootPath}/.vscode/openocd.cfg`] + const openocdPath = join(process.env.packagePath || '', 'openocd') + const openocd = systemFilter(join(openocdPath, 'openocd.exe'), join(openocdPath, 'openocd'), join(openocdPath, 'openocd')) + const openocdService = spawn(openocd, args) + openocdService.stdout.on('data', throttleOffset(data => { + this.logger.info(data) + }, 10, 5000)) + openocdService.stderr.on('data', throttleOffset(data => { + this.logger.info(data) + }, 10, 5000)) + openocdService.on('error', throttleOffset(err => { + this.logger.error(err.message) + }, 10, 5000)) + openocdService.on('close', code => { + if (code === 0) + this.logger.info(`Command execution completed with code: ${code}`) + else { + this.logger.error(`Command execution failed with code: ${code}`) + if (code === 1) { + this.clearFunc() + this.logger.show() + vscode.window.showErrorMessage('Openocd service start failed. Please check openocd output channel.') + } + } + }) + return openocdService.pid + } + + public kill(): void { + if (/^win/.test(process.platform)) { + spawn("taskkill", ["/PID", this.pid.toString(), "/T", "/F"]) + + // 确保 kill 掉 openocd + const killProcess = spawn('taskkill', ['/IM', '"openocd.exe"', '/F']) + killProcess.stdout.on('data', data => this.logger.info(data)) + killProcess.stderr.on('data', data => this.logger.error(data)) + } else { + process.kill(this.pid, 'SIGTERM') + + // 确保 kill 掉 openocd + const killProcess = spawn('killall', ['SIGKILL', 'openocd']) + killProcess.stdout.on('data', data => this.logger.info(data)) + killProcess.stderr.on('data', data => this.logger.error(data)) + } + } +} \ No newline at end of file diff --git a/src/service/serialport.ts b/src/service/serialport.ts new file mode 100644 index 0000000..576a939 --- /dev/null +++ b/src/service/serialport.ts @@ -0,0 +1,124 @@ +import * as vscode from 'vscode' +import * as SerialPort from 'serialport' +import { EventEmitter } from 'events' +import * as os from 'os' +import { join } from 'path' +import { constants } from 'fs' +import { accessAsync, sudoexecPromisify } from '@utils/index' + +export class SerialPortService extends EventEmitter { + private outputStatus: boolean + private updateStatusbar: any + private connection?: SerialPort + private readonly extensionPath: string + + constructor(updateStatusbar: any, extensionPath: string) { + super() + this.outputStatus = false + this.updateStatusbar = updateStatusbar + this.extensionPath = extensionPath + } + + get status() { + return this.outputStatus + } + + /* + * There is a bug that when serialport's 'data' and 'error' listener didn't open, 'close' listener will never recive callback. + * So Please use SerialPortService.on() + * It will never return serialport service. + */ + + public setStatus(value: boolean) { + this.outputStatus = value + this.updateStatusbar(value) + + // Reset Serialport when output channel open. + if (this.connection) { + this.resetSerialPort() + } + } + public createSerialPortConnect(device: string) { + return new Promise(async (resolve, reject) => { + const readPermission = await accessAsync(device, constants.R_OK) + const writePermission = await accessAsync(device, constants.W_OK) + if (os.platform() === 'linux' && (!readPermission || !writePermission)) { + await sudoexecPromisify(`chmod 666 ${device}`, {name: 'Kendryte Dev Tool', icns: join(this.extensionPath, 'resources', 'kendryte.svg')}) + } + const sp = new SerialPort(device, { + baudRate: 115200, + autoOpen: true, + lock: true, + rtscts: false, + xon: true, + xoff: true, + xany: true, + }, err => { + sp.set({ + dtr: false, + rts: false, + dsr: false, + cts: false, + brk: false + }) + if (!err) { + this.connection = sp + resolve() + } else { + reject(err) + } + }) + sp.setEncoding('utf8') + sp.on('data', _ => { + this.emit('data', _) + }) + sp.on('error', _ => { + this.emit('error', _) + }) + sp.on('close', _ => { + this.emit('close', _) + }) + }) + } + public disposeSerialPort() { + if (this.connection) { + this.connection.close() + this.connection.removeAllListeners() + this.removeAllListeners() + } + delete this.connection + this.setStatus(false) + } + + // Only can be used in upload + public disconnectService() { + return new Promise(resolve => { + if (this.connection) { + this.connection.close(_ => { + if (this.connection) { + this.connection.removeAllListeners() + } + this.removeAllListeners() + delete this.connection + resolve() + }) + } + }) + } + + private async resetSerialPort() { + if (this.connection) { + this.connection.close(_ => { + if (this.connection) { + this.createSerialPortConnect(this.connection.path) + .then() + .catch(err => { + vscode.window.showErrorMessage(err.message) + }) + } + }) + this.connection.removeAllListeners() + this.removeAllListeners() + } + } +} \ No newline at end of file diff --git a/src/test/runTest.ts b/src/test/runTest.ts new file mode 100644 index 0000000..1eabfa3 --- /dev/null +++ b/src/test/runTest.ts @@ -0,0 +1,23 @@ +import * as path from 'path'; + +import { runTests } from 'vscode-test'; + +async function main() { + try { + // The folder containing the Extension Manifest package.json + // Passed to `--extensionDevelopmentPath` + const extensionDevelopmentPath = path.resolve(__dirname, '../../'); + + // The path to test runner + // Passed to --extensionTestsPath + const extensionTestsPath = path.resolve(__dirname, './suite/index'); + + // Download VS Code, unzip it and run the integration test + await runTests({ extensionDevelopmentPath, extensionTestsPath }); + } catch (err) { + console.error('Failed to run tests'); + process.exit(1); + } +} + +main(); diff --git a/src/test/suite/extension.test.ts b/src/test/suite/extension.test.ts new file mode 100644 index 0000000..820cf90 --- /dev/null +++ b/src/test/suite/extension.test.ts @@ -0,0 +1,18 @@ +import * as assert from 'assert'; +import { before } from 'mocha'; + +// You can import and use all API from the 'vscode' module +// as well as import your extension to test it +import * as vscode from 'vscode'; +// import * as myExtension from '../extension'; + +suite('Extension Test Suite', () => { + before(() => { + vscode.window.showInformationMessage('Start all tests.'); + }); + + test('Sample test', () => { + assert.equal(-1, [1, 2, 3].indexOf(5)); + assert.equal(-1, [1, 2, 3].indexOf(0)); + }); +}); diff --git a/src/test/suite/index.ts b/src/test/suite/index.ts new file mode 100644 index 0000000..2cd152c --- /dev/null +++ b/src/test/suite/index.ts @@ -0,0 +1,37 @@ +import * as path from 'path'; +import * as Mocha from 'mocha'; +import * as glob from 'glob'; + +export function run(): Promise { + // Create the mocha test + const mocha = new Mocha({ + ui: 'tdd', + }); + mocha.useColors(true); + + const testsRoot = path.resolve(__dirname, '..'); + + return new Promise((c, e) => { + glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { + if (err) { + return e(err); + } + + // Add files to the test suite + files.forEach(f => mocha.addFile(path.resolve(testsRoot, f))); + + try { + // Run the mocha test + mocha.run(failures => { + if (failures > 0) { + e(new Error(`${failures} tests failed.`)); + } else { + c(); + } + }); + } catch (err) { + e(err); + } + }); + }); +} diff --git a/src/tsconfig.json b/src/tsconfig.json new file mode 100644 index 0000000..c68278e --- /dev/null +++ b/src/tsconfig.json @@ -0,0 +1,41 @@ +{ + "compilerOptions": { + "types": [ + "glob", + "jszip", + "json5", + "lodash", + "minimatch", + "module-alias", + "node", + "prop-types", + "serialport", + "split2", + "node-7z" + ], + "experimentalDecorators": true, + "module": "commonjs", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "target": "es6", + "outDir": "../build", + "lib": [ + "es6", + "webworker" + ], + "sourceMap": true, + "strict": true, + "baseUrl": ".", + "paths": { + "@command/*": ["command/*"], + "@utils/*": ["utils/*"], + "@debug/*": ["debug/*"], + "@service/*": ["service/*"], + "@common/*": ["common/*"], + "@treeview/*": ["views/*"] + } + }, + "include": [ + "*" + ] +} diff --git a/src/utils/access.ts b/src/utils/access.ts new file mode 100644 index 0000000..2b1df06 --- /dev/null +++ b/src/utils/access.ts @@ -0,0 +1,9 @@ +import { access, PathLike } from 'fs' + +export const accessAsync = (path: PathLike, mode: number | undefined): Promise => { + return new Promise(resolve => { + access(path, mode, err => { + resolve(!err) + }) + }) +} \ No newline at end of file diff --git a/src/utils/archiveTool.ts b/src/utils/archiveTool.ts new file mode 100644 index 0000000..56a4d2e --- /dev/null +++ b/src/utils/archiveTool.ts @@ -0,0 +1,45 @@ +import { unlinkPromisify } from '@utils/promisify' +import { extract, I7zHandler, compress } from '7zip-bin-wrapper' + +export const unArchive = (sourceFile: string, target: string): Promise => { + return new Promise(async (resolve, reject) => { + try { + await waitHandle(extract(sourceFile, target)) + } catch(e) { + reject(e) + return + } + try { + await unlinkPromisify(sourceFile) + } catch(e) { + console.log(e) + } + resolve() + }) +} + +export const archive = (targetFile: string, source: string, extraSource: string[] = []): Promise => { + return new Promise(async (resolve, reject) => { + try { + await waitHandle(compress(targetFile, source, ...extraSource)) + } catch(e) { + reject(e) + return + } + resolve() + }) +} + +const waitHandle = (handler: I7zHandler) => { + // console.log(handler.commandline.join(' ')); + handler.on('output', (data: string) => { + // console.log(data) + }); + handler.on('progress', ({progress, message}) => { + // logger.progress(progress); + // logger.sub1(progress.toFixed(0) + '%'); + // logger.sub2(message); + }); + + return handler.promise(); +} \ No newline at end of file diff --git a/src/utils/baseLogger.ts b/src/utils/baseLogger.ts new file mode 100644 index 0000000..3c439b1 --- /dev/null +++ b/src/utils/baseLogger.ts @@ -0,0 +1,95 @@ +import { format } from 'util' + +export enum LogLevel { + Trace, + Debug, + Info, + Warning, + Error, + Critical, + Off +} + +export const LogLevelNames = { + [LogLevel.Trace]: 'TRACE', + [LogLevel.Debug]: 'DEBUG', + [LogLevel.Info]: ' INFO', + [LogLevel.Warning]: ' WARN', + [LogLevel.Error]: 'ERR', + [LogLevel.Critical]: 'FATAL', + [LogLevel.Off]: 'OFF' +} + +export interface IMyLogger { + writeln(data: string, ...args: any[]): any + + trace(msg: string, ...args: any[]): any + debug(msg: string, ...args: any[]): any + info(msg: string, ...args: any[]): any + warning(msg: string, ...args: any[]): any + error(msg: string, ...args: any[]): any + critical(msg: string, ...args: any[]): any +} + +export abstract class NodeLoggerCommon implements IMyLogger { + protected constructor(private readonly _tag: string) { + } + + abstract clear(): void + + protected abstract printLine(tag: string, level: LogLevel, message: string): any + + protected prependTags(tag: string, message: string) { + return message.replace(/^/g, `[${tag}] `).trim() + } + + writeln(msg: string, ...args: any[]) { + if (args.length) { + msg = format(msg, ...args) + } + this.printLine(this._tag, LogLevel.Off, msg) + } + + trace(msg: string, ...args: any[]) { + if (args.length) { + msg = format(msg, ...args) + } + this.printLine(this._tag, LogLevel.Trace, msg) + } + + debug(msg: string, ...args: any[]) { + if (args.length) { + msg = format(msg, ...args) + } + this.printLine(this._tag, LogLevel.Debug, msg) + } + + info(msg: string, ...args: any[]) { + if (args.length) { + msg = format(msg, ...args) + } + this.printLine(this._tag, LogLevel.Info, msg) + } + + warning(msg: string, ...args: any[]) { + if (args.length) { + msg = format(msg, ...args) + } + this.printLine(this._tag, LogLevel.Warning, msg) + } + + error(msg: string, ...args: any[]) { + if (args.length) { + msg = format(msg, ...args) + } + this.printLine(this._tag, LogLevel.Error, msg) + } + + critical(msg: string, ...args: any[]) { + if (args.length) { + msg = format(msg, ...args) + } + this.printLine(this._tag, LogLevel.Critical, msg) + throw new Error(msg) + } +} diff --git a/src/utils/configreader.ts b/src/utils/configreader.ts new file mode 100644 index 0000000..838f09e --- /dev/null +++ b/src/utils/configreader.ts @@ -0,0 +1,11 @@ +import { readFileSync } from 'fs' + +export const configReader = (filePath: string): T | undefined => { + try { + const data = readFileSync(filePath, 'utf-8') + const reader: T = JSON.parse(data) + return reader + } catch(e) { + return + } +} \ No newline at end of file diff --git a/src/utils/directory.ts b/src/utils/directory.ts new file mode 100644 index 0000000..d83ec79 --- /dev/null +++ b/src/utils/directory.ts @@ -0,0 +1,24 @@ +import * as fs from 'fs' +import * as path from 'path' +import { readdirPromisify } from './promisify' +export const removeDir = (p: string) => { + return new Promise((resolve, reject) => { + fs.stat(p, (err, statObj) => { + if (err) { + console.log(err) + resolve() + return + } + if (statObj.isDirectory()) { + readdirPromisify(p) + .then(async dirs => { + await Promise.all(dirs.map(async dir => await removeDir(path.join(p, dir)))) + fs.rmdir(p, resolve) + }) + } else { + fs.unlink(p, resolve) + } + + }) + }) +} \ No newline at end of file diff --git a/src/utils/download.ts b/src/utils/download.ts new file mode 100644 index 0000000..a5d3c5c --- /dev/null +++ b/src/utils/download.ts @@ -0,0 +1,32 @@ +import Axios, { AxiosResponse } from 'axios'; +import { mkdirPromisify } from '@utils/promisify' +import * as fs from 'fs' +import { join } from 'path' + +export const downloadPackage = (path: string, url: string, fileName: string): Promise => { + return new Promise(async (resolve, reject) => { + let res: AxiosResponse + try { + res = await Axios({ + method: "get", + url, + responseType: "stream" + }) + } catch(e) { + reject(e) + return + } + try { + await mkdirPromisify(path, { recursive: true }) + } catch(e) { + + } + const writer = fs.createWriteStream(join(path, fileName)) + res.data.pipe(writer) + writer.on('finish', msg => { + // console.log(`finish write file ${fileName}`) + resolve(msg) + }) + writer.on('error', e => reject(e)) + }) +} \ No newline at end of file diff --git a/src/utils/extensionLogger.ts b/src/utils/extensionLogger.ts new file mode 100644 index 0000000..07cec18 --- /dev/null +++ b/src/utils/extensionLogger.ts @@ -0,0 +1,63 @@ +import * as vscode from 'vscode' +import { LogLevel, LogLevelNames, NodeLoggerCommon } from '@utils/baseLogger' + +let globalChannel: vscode.OutputChannel +const channels: Array = [] + +function globalLogChannelSingleton(channelName: string) { + if (channels.indexOf(channelName) === -1) { + globalChannel = vscode.window.createOutputChannel(channelName) + channels.push(channelName) + } + return globalChannel +} + +export function disposeChannel() { + globalChannel.appendLine('will dispose') + globalChannel.dispose() +} + +export class FrontendChannelLogger extends NodeLoggerCommon { + private currentLevel!: LogLevel + + constructor( + tag: string, + channelName: string, + private readonly channel: vscode.OutputChannel = globalLogChannelSingleton(channelName), + ) { + super(tag) + this.setLevel(LogLevel.Info) + } + + public setLevel(logLevel: LogLevel) { + this.currentLevel = logLevel + } + + clear() { + this.channel.clear() + } + + show() { + this.channel.show() + } + + protected printLine(tag: string, level: LogLevel, message: string) { + if (this.currentLevel === LogLevel.Off || this.currentLevel > level) { + return + } + if (message === '') { + this.channel.appendLine('') + return + } + + if (typeof message !== 'string') { + message = '' + message + } + + const levelName = LogLevelNames[level] + const fullTag = levelName ? `${tag}][${levelName}` : tag + this.channel.appendLine(this.prependTags(fullTag, message)) + } +} + + diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..72cdc69 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,13 @@ +export * from './sysinfo' +export * from './download' +export * from './archiveTool' +export * from './stringFormat' +export * from './extensionLogger' +export * from './baseLogger' +export * from './promisify' +export * from './configreader' +export * from './throttleoffset' +export * from './jszipWrapper' +export * from './urlJoin' +export * from './directory' +export * from './access' \ No newline at end of file diff --git a/src/utils/jszipWrapper.ts b/src/utils/jszipWrapper.ts new file mode 100644 index 0000000..05b3b0a --- /dev/null +++ b/src/utils/jszipWrapper.ts @@ -0,0 +1,15 @@ +import { mkdirPromisify, unlinkPromisify } from '@utils/index' +import * as AdmZip from 'adm-zip' + +export const jszipUnarchive = (filePath: string, targetPath: string): Promise => { + return new Promise(async (resolve, reject) => { + const zipFile = new AdmZip(filePath) + zipFile.extractAllTo(targetPath, true) + try { + await unlinkPromisify(filePath) + } catch (e) { + + } + resolve() + }) +} \ No newline at end of file diff --git a/src/utils/normalizeArray.ts b/src/utils/normalizeArray.ts new file mode 100644 index 0000000..ef33337 --- /dev/null +++ b/src/utils/normalizeArray.ts @@ -0,0 +1,7 @@ +export function normalizeArray(input: any): T[] { + if (input && Array.isArray(input)) { + return input; + } else { + return []; + } +} \ No newline at end of file diff --git a/src/utils/promisify.ts b/src/utils/promisify.ts new file mode 100644 index 0000000..e69194a --- /dev/null +++ b/src/utils/promisify.ts @@ -0,0 +1,26 @@ +import { promisify } from 'util' +import { mkdir, writeFile, unlink, readdir, readFile } from 'fs' +import { exec } from 'child_process' +import * as sudo from 'sudo-prompt' +import { EventEmitter } from 'events' + +export const execPromisify = promisify(exec) +export const mkdirPromisify = promisify(mkdir) +export const writeFilePromisify = promisify(writeFile) +export const unlinkPromisify = promisify(unlink) +export const readdirPromisify = promisify(readdir) +export const readFilePromisify = promisify(readFile) +export const sudoexecPromisify = (cmd: string, options: {name?: string, icns?: string}): Promise => { + return new Promise((resolve, reject) => { + const event = new EventEmitter() + sudo.exec(cmd, options, (err, stdout, stderr) => { + if (err) { + reject(err) + return + } + event.emit('stdout', stdout) + event.emit('stderr', stderr) + resolve(event) + }) + }) +} \ No newline at end of file diff --git a/src/utils/stringFormat.ts b/src/utils/stringFormat.ts new file mode 100644 index 0000000..f58c63e --- /dev/null +++ b/src/utils/stringFormat.ts @@ -0,0 +1,22 @@ +/* + Warning: This format function must be used as: + + format(` + // content + `) + + The first content line's indentation is necessary. + The following line's indentation is based on the first line which have indentation. +*/ +export const format = (str: string) => { + let baseTab = 0 + let firstLine = true + return str.replace(/\n(\s+)/g, (m, m1) => { + if (firstLine) { + baseTab = m1.length + firstLine = !firstLine + return m1.slice(baseTab) + } + return "\n" + m1.slice(baseTab) + }) +} \ No newline at end of file diff --git a/src/utils/sysinfo.ts b/src/utils/sysinfo.ts new file mode 100644 index 0000000..944c7fb --- /dev/null +++ b/src/utils/sysinfo.ts @@ -0,0 +1,28 @@ +import * as os from 'os' +import { join } from 'path' +export const systemFilter = (win32: T,darwin: T, linux: T): T => { + const platform = os.platform() + switch(platform) { + case 'win32': { + return win32 + } + case 'darwin': { + return darwin + } + case 'linux': { + return linux + } + default: { + return linux + } + }; +} + +export const setPackagePath = (): string => { + const win32 = join(process.env['USERPROFILE'] || '', '.k210-extension') + const darwin = join(process.env['HOME'] || '', '.k210-extension') + const linux = join(process.env['HOME'] || '', '.k210-extension') + const path = systemFilter(win32, darwin, linux) + process.env.packagePath = path + return process.env.packagePath +} \ No newline at end of file diff --git a/src/utils/throttleoffset.ts b/src/utils/throttleoffset.ts new file mode 100644 index 0000000..141d106 --- /dev/null +++ b/src/utils/throttleoffset.ts @@ -0,0 +1,20 @@ +import { throttle } from 'lodash' +export const throttleOffset = any>(func: T, gapTime: number, offset: number): (...args: any) => void => { + let enableThrottle = false + let timer: NodeJS.Timeout + let handler = func + const throttleHandler = throttle(func, gapTime) + return (...args) => { + if (!timer) { + timer = global.setTimeout(() => { + enableThrottle = true + }, offset) + } + + if (enableThrottle) { + throttleHandler(...args) + } else { + handler(...args) + } + } +} \ No newline at end of file diff --git a/src/utils/urlJoin.ts b/src/utils/urlJoin.ts new file mode 100644 index 0000000..ff2a757 --- /dev/null +++ b/src/utils/urlJoin.ts @@ -0,0 +1,7 @@ +import * as url from 'url' +import { join } from 'path' +export const urlJoin = (host: string, ...args: Array): string => { + const path = join(...args) + const fullPath = url.resolve(host, path) + return fullPath +} \ No newline at end of file diff --git a/src/views/DevPackages.ts b/src/views/DevPackages.ts new file mode 100644 index 0000000..dbd1f27 --- /dev/null +++ b/src/views/DevPackages.ts @@ -0,0 +1,108 @@ +import * as vscode from 'vscode'; +import * as fs from 'fs'; +import * as path from 'path'; + +export class DevPackages implements vscode.TreeDataProvider { + + private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); + readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; + + constructor(private workspaceRoot: string | undefined) { + + } + + refresh(): void { + console.log('refresh') + this._onDidChangeTreeData.fire(); + } + + getTreeItem(element: Dependency): vscode.TreeItem { + return element; + } + + getChildren(element?: Dependency): Thenable { + if (!this.workspaceRoot) { + vscode.window.showInformationMessage('No dependency in empty workspace'); + return Promise.resolve([]); + } + + if (element) { + return Promise.resolve(this.getDepsInPackageJson(path.join(this.workspaceRoot, 'kendryte_libraries', element.label, 'kendryte-package.json'))); + } else { + const packageJsonPath = path.join(this.workspaceRoot, 'kendryte-package.json'); + if (this.pathExists(packageJsonPath)) { + return Promise.resolve(this.getDepsInPackageJson(packageJsonPath)); + } else { + vscode.window.showInformationMessage('Workspace has no kendryte-package.json'); + return Promise.resolve([]); + } + } + + } + + /** + * Given the path to package.json, read all its dependencies and devDependencies. + */ + private getDepsInPackageJson(packageJsonPath: string): Dependency[] { + if (this.pathExists(packageJsonPath)) { + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); + + const toDep = (moduleName: string, version: string): Dependency => { + if (this.pathExists(path.join(this.workspaceRoot, 'kendryte_libraries', moduleName))) { + return new Dependency(moduleName, version, vscode.TreeItemCollapsibleState.None); + } else { + return new Dependency(moduleName, version, vscode.TreeItemCollapsibleState.None, { + command: 'extension.openPackageOnNpm', + title: '', + arguments: [moduleName] + }); + } + }; + + const deps = packageJson.dependencies + ? Object.keys(packageJson.dependencies).map(dep => toDep(dep, packageJson.dependencies[dep])) + : [] + return deps + } else { + return []; + } + } + + private pathExists(p: string): boolean { + try { + fs.accessSync(p); + } catch (err) { + return false; + } + + return true; + } +} + +export class Dependency extends vscode.TreeItem { + + constructor( + public readonly label: string, + private version: string, + public readonly collapsibleState: vscode.TreeItemCollapsibleState, + public readonly command?: vscode.Command + ) { + super(label, collapsibleState); + } + + get tooltip(): string { + return `${this.label}-${this.version}`; + } + + get description(): string { + return this.version; + } + + iconPath = { + light: path.join(__filename, '..', '..', '..', 'resources', 'light', 'dependency.svg'), + dark: path.join(__filename, '..', '..', '..', 'resources', 'dark', 'dependency.svg') + }; + + contextValue = 'dependency'; + +} \ No newline at end of file diff --git a/tslint.json b/tslint.json new file mode 100644 index 0000000..3c87982 --- /dev/null +++ b/tslint.json @@ -0,0 +1,20 @@ +{ + "rules": { + "no-string-throw": true, + "no-unused-expression": true, + "no-duplicate-variable": true, + "curly": true, + "class-name": true, + "semicolon": [ + false, + "always" + ], + "triple-equals": true, + "react-hooks/rules-of-hooks": "error", + "react-hooks/exhaustive-deps": "warn" + }, + "defaultSeverity": "warning", + "plugins": [ + "react-hooks" + ] +} diff --git a/view-src/App.css b/view-src/App.css new file mode 100644 index 0000000..2da96f1 --- /dev/null +++ b/view-src/App.css @@ -0,0 +1,16 @@ +.App { + display: flex; + height: 100vh; + overflow: hidden; + text-align: center; +} + +.container { + flex-grow: 1; + overflow: scroll; +} + +@keyframes App-logo-spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} diff --git a/view-src/App.test.tsx b/view-src/App.test.tsx new file mode 100644 index 0000000..e0f09ab --- /dev/null +++ b/view-src/App.test.tsx @@ -0,0 +1,9 @@ +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import App from './App'; + +it('renders without crashing', () => { + const div = document.createElement('div'); + ReactDOM.render(, div); + ReactDOM.unmountComponentAtNode(div); +}); diff --git a/view-src/App.tsx b/view-src/App.tsx new file mode 100644 index 0000000..8c4b6c4 --- /dev/null +++ b/view-src/App.tsx @@ -0,0 +1,43 @@ +import * as React from 'react' + +import './App.css' +import SideBar from 'components/SideBar' +import HomePage from 'components/HomePage' +import Libraries from 'components/Libraries' + + +const App = () => { + const [router, setRouter] = React.useState('/') + const renderPage = () => { + switch (router) { + case '/': + return ( + + ) + case '/examples': + return ( + + ) + case '/libraries': + return ( + + ) + default: + return ( + + ) + } + } + return ( +
+ +
+ { + renderPage() + } +
+
+ ) +} + +export default App \ No newline at end of file diff --git a/view-src/components/HomePage/HomePage.scss b/view-src/components/HomePage/HomePage.scss new file mode 100644 index 0000000..af4d611 --- /dev/null +++ b/view-src/components/HomePage/HomePage.scss @@ -0,0 +1,76 @@ +.homepage { + // min-width: 900px; + padding: 8vh 8vw 0; + color: #fff; + overflow: scroll; + &-main { + display: flex; + align-items: center; + flex-direction: column; + &-quickaccess { + display: flex; + flex-direction: column; + align-items: center; + font-size: 22px; + font-weight: 500; + color: #ccc; + text-align: left; + .accesses { + margin-top: 2.5vh; + button { + box-sizing: border-box; + display: flex; + align-items: center; + width: 17.85vw; + min-width: 250px; + height: 2.85vw; + min-height: 40px; + margin-bottom: 1.25vh; + padding: 0 15px; + border: 1px solid #6c6c6c; + border-radius: 4px; + background: transparent; + color: #989898; + font-size: 16px; + outline: none; + img { + width: 1.14vw; + min-width: 16px; + margin-right: 0.6vw; + filter: grayscale(100%); + } + &:hover { + border-color: #249edc; + color: #249edc; + cursor: pointer; + transition: 0.2s; + img { + filter: grayscale(0%); + transition: 0.2s; + } + } + &:last-child { + margin-bottom: 0; + } + } + } + } + img { + width: 256px; + } + &-version { + margin-top: 1.6vh; + color: #989898; + font-size: 16px; + user-select: none; + span { + margin-left: 6px; + padding: 2px 4px; + background: #fff; + border-radius: 4px; + color: #000; + font-weight: 700; + } + } + } +} diff --git a/view-src/components/HomePage/index.tsx b/view-src/components/HomePage/index.tsx new file mode 100644 index 0000000..1e71448 --- /dev/null +++ b/view-src/components/HomePage/index.tsx @@ -0,0 +1,39 @@ +import * as React from 'react' +import Add from 'images/add.svg' +import Example from 'images/project-blue.svg' +import Libraries from 'images/libraries-blue.svg' +import kendryteLogo from 'images/kendryte.svg' +import { webviewRequest } from 'utils/webviewRequest' + +import 'components/HomePage/HomePage.scss' + +const HomePage = (props: any) => { + const linkTo = (path: string) => { + props.setRouter(path) + } + const createNewProject = () => { + webviewRequest({ + type: 'create' + }) + } + return ( +
+
+ logo +
+ Current version + 0.1.0 +
+
+
+ + + +
+
+
+
+ ) +} + +export default HomePage \ No newline at end of file diff --git a/view-src/components/ItemCard/ItemCard.scss b/view-src/components/ItemCard/ItemCard.scss new file mode 100644 index 0000000..2225b45 --- /dev/null +++ b/view-src/components/ItemCard/ItemCard.scss @@ -0,0 +1,82 @@ +.itemcard { + margin-top: 0.57vw; + width: 100%; + border: 1px solid #414141; + border-radius: 4px; + background-color: #252526; + color: #989898; + font-variant: tabular-nums; + &-header { + display: flex; + justify-content: space-between; + padding: 1.6vh 2.28vw; + border-bottom: 1px solid #414141; + background-color: #2d2d2d; + &-left { + &-package { + margin-right: 0.28vw; + font-size: 16px; + color: #249edc; + } + &-author { + margin-left: 0.28vw; + } + } + &-right { + display: flex; + align-items: center; + img { + width: 1.2vw; + margin-right: 0.5vw; + } + } + } + &-body { + padding: 1.6vh 2.28vw; + text-align: left; + &-desc { + font-size: 14px; + } + &-tags { + display: flex; + align-items: center; + margin-top: 0.84vh; + img { + width: 1.5vw; + min-width: 21px; + margin-right: 0.5vw; + } + .tag { + margin-right: 0.4vw; + color: #249edc; + cursor: pointer; + user-select: none; + &:last-child { + margin-right: 0; + } + &:hover { + color: #80cdf3; + } + } + } + &-install { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 1.6vh; + button { + padding: 8px 12px; + border: 0; + border-radius: 0.28vw; + background-color: #249edc; + color: #fff; + outline: none; + cursor: pointer; + &:hover { + transition: 0.2s; + opacity: 0.8; + } + } + } + } +} \ No newline at end of file diff --git a/view-src/components/ItemCard/index.tsx b/view-src/components/ItemCard/index.tsx new file mode 100644 index 0000000..1644724 --- /dev/null +++ b/view-src/components/ItemCard/index.tsx @@ -0,0 +1,79 @@ +import * as React from 'react' +import tagLogo from 'images/tag.svg' +import coreLogo from 'images/core.svg' +import Hook from 'images/hook.svg' +import { webviewRequest } from 'utils/webviewRequest' +import Loading from 'components/common/Loading' + +import 'components/ItemCard/ItemCard.scss' + +const ItemCard = (props: any) => { + const [installStatus, setInstallStatus] = React.useState(0) + const renderTags = () => { + return props.library.tags.map((tag: string) => { + return {props.setKeyword(tag)}}>{tag} + }) + } + const postInstallMsg = () => { + if (props.type === 'package') setInstallStatus(1) + webviewRequest({ + type: props.type, + name: props.library.name + }) + .then(data => { + console.log(data) + if (props.type === 'package') setInstallStatus(2) + }) + .catch(error => { + console.log('error!') + if (props.type === 'package') setInstallStatus(0) + }) + } + const renderButton = () => { + if (props.installed) { + return logo + } + switch(installStatus) { + case 0: + return + case 1: + return + case 2: + return logo + default: + return + } + } + return ( +
+
+
+ {props.library.name} + by + {props.library.author} +
+
+ logo + {props.library.board} +
+
+
+ {props.library.description} +
+ logo + { + renderTags() + } +
+
+ Version: {Object.keys(props.library.versions)[0]} + { + renderButton() + } +
+
+
+ ) +} + +export default ItemCard \ No newline at end of file diff --git a/view-src/components/Libraries/Libraries.scss b/view-src/components/Libraries/Libraries.scss new file mode 100644 index 0000000..1515cbc --- /dev/null +++ b/view-src/components/Libraries/Libraries.scss @@ -0,0 +1,39 @@ +.libraries { + flex-grow: 1; + padding: 4vh 8vw; + overflow: scroll; + &-input { + margin-bottom: 2.28vw; + input { + box-sizing: border-box; + width: 100%; + min-width: 900px; + height: 3.8vh; + min-height: 30px; + padding: 0.6vh 0.857vw; + border: 1px solid #6c6c6c; + border-radius: 4px; + background-color: #2d2d2d; + color: #989898; + &:focus { + border-color: #249edc; + transition: 0.2s; + outline: none; + } + } + } + &-list { + min-width: 900px; + .loading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: 16px; + color: #989898; + span { + margin-top: 2.8vh; + } + } + } +} \ No newline at end of file diff --git a/view-src/components/Libraries/index.tsx b/view-src/components/Libraries/index.tsx new file mode 100644 index 0000000..28abdd8 --- /dev/null +++ b/view-src/components/Libraries/index.tsx @@ -0,0 +1,106 @@ +import * as React from 'react' +import ItemCard from 'components/ItemCard' +import Loading from 'components/common/Loading' +import Error from 'components/common/Error' +import { filter } from 'utils/filter' +import { webviewRequest } from 'utils/webviewRequest' +import Axios from 'axios' + +import 'components/Libraries/Libraries.scss' + +const Libraries = (props: any) => { + const [keyword, setKeyword] = React.useState('') + const [libraryList, setLibraryList] = React.useState({}) + const [requestStatus, setRequestStatus] = React.useState('pending') + const [localDependencies, setLocalDependencies] = React.useState([] as Array) + + const getData = () => { + setRequestStatus('pending') + Axios({ + method: "get", + url: `https://mirrors-kendryte.s3.cn-northwest-1.amazonaws.com.cn/${props.type}/list.json`, + responseType: "json" + }).then(res => { + if (res.status === 200) { + const type = props.type === 'example' ? 'examples' : 'packages' + setLibraryList(res.data[type]) + setRequestStatus('success') + } else { + setRequestStatus('server error') + } + }).catch(error => { + setRequestStatus('network error') + }) + } + + // First request + React.useEffect(() => { + getData() + setKeyword('') + // Check local dependencies + if (props.type === 'package') { + webviewRequest({ + type: 'check' + }) + .then(data => { + setLocalDependencies(data.dependencies) + }) + } + // eslint-disable-next-line + }, [props.type]) + + const libraries = filter(keyword, libraryList) + + const renderLibraries = () => { + switch (requestStatus) { + case 'pending': + return ( +
+ + Loading +
+ ) + case 'network error': + return ( +
+ +
+ ) + case 'server error': + return ( + + ) + default: + return Object.keys(libraries).map(library => { + return ( + = 0} + /> + ) + }) + } + } + + return ( +
+
+ setKeyword(event.target.value)} + placeholder="Search..." + /> +
+
+ { + renderLibraries() + } +
+
+ ) +} + +export default Libraries \ No newline at end of file diff --git a/view-src/components/SideBar/SideBar.scss b/view-src/components/SideBar/SideBar.scss new file mode 100644 index 0000000..55e4d48 --- /dev/null +++ b/view-src/components/SideBar/SideBar.scss @@ -0,0 +1,44 @@ +.side-bar { + flex-grow: 0; + flex-shrink: 0; + width: 5vw; + min-width: 70px; + max-width: 90px; + height: 100vh; + background: rgb(50, 50, 50); + .side-bar-item { + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + height: 5vw; + min-height: 70px; + max-height: 90px; + opacity: 0.5; + &.top { + opacity: 1; + } + &.current { + opacity: 1; + background-color: #249edc; + } + .kendryte { + width: 2.85vw; + min-width: 40px; + max-width: 50px; + } + img { + width: 1.71vw; + min-width: 24px; + max-width: 30px; + } + span { + color: #fff; + } + } + .side-bar-item:hover { + transition: 0.2s; + opacity: 1; + cursor: pointer; + } +} diff --git a/view-src/components/SideBar/index.tsx b/view-src/components/SideBar/index.tsx new file mode 100644 index 0000000..a967a9a --- /dev/null +++ b/view-src/components/SideBar/index.tsx @@ -0,0 +1,38 @@ +import * as React from 'react' + +import './SideBar.scss' +import kendryteLogo from 'images/kendryte.svg' +import librariesLogo from 'images/libraries.svg' +import projectLogo from 'images/project.svg' + +const SideBar = (props: any) => { + const sideItems = { + "Libraries": librariesLogo, + "Examples": projectLogo + } + const linkTo = (path: string) => { + props.setRouter(path) + } + const renderSideItems = () => { + return Object.keys(sideItems).map(item => { + return ( +
linkTo(`/${item.toLowerCase()}`)} > + logo + {item} +
+ ) + }) + } + return ( +
+
linkTo('/')}> + logo +
+ { + renderSideItems() + } +
+ ) +} + +export default SideBar \ No newline at end of file diff --git a/view-src/components/common/Error/Error.scss b/view-src/components/common/Error/Error.scss new file mode 100644 index 0000000..94e4076 --- /dev/null +++ b/view-src/components/common/Error/Error.scss @@ -0,0 +1,19 @@ +.error { + &-title { + font-size: 4vw; + font-family: "ProximaNovaRgBold","Helvetica Neue",helvetica,arial,sans-serif; + color: #fff; + } + &-logo { + height: 4vw; + } + &-info { + font-size: 1vw; + font-weight: 500; + color: #989898; + span { + color: #249edc; + cursor: pointer; + } + } +} \ No newline at end of file diff --git a/view-src/components/common/Error/index.tsx b/view-src/components/common/Error/index.tsx new file mode 100644 index 0000000..5d82656 --- /dev/null +++ b/view-src/components/common/Error/index.tsx @@ -0,0 +1,27 @@ +import * as React from 'react' +import networkError from 'images/network-error.svg' +import serverError from 'images/server-error.svg' + +import 'components/common/Error/Error.scss' + +const Error = (props: any) => { + const logo = props.type === 'network' ? networkError : serverError + return ( +
+ logo + { + props.type === 'network' + ? +

+ It seems like something wrong with your network. Please retry. +

+ : +

+ It seems like something wrong with out server. Please wait for a while or retry. +

+ } +
+ ) +} + +export default Error \ No newline at end of file diff --git a/view-src/components/common/Loading/Loading.scss b/view-src/components/common/Loading/Loading.scss new file mode 100644 index 0000000..491493a --- /dev/null +++ b/view-src/components/common/Loading/Loading.scss @@ -0,0 +1,34 @@ +.lds-ring { + display: inline-block; + position: relative; + width: 4vw; + height: 4vw; + div { + box-sizing: border-box; + display: block; + position: absolute; + width: 4vw; + height: 4vw; + border: 4px solid #989898; + border-radius: 50%; + animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite; + border-color: #989898 transparent transparent transparent; + &:nth-child(1) { + animation-delay: -0.45s; + } + &:nth-child(2) { + animation-delay: -0.3s; + } + &:nth-child(3) { + animation-delay: -0.15s; + } + } +} +@keyframes lds-ring { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} diff --git a/view-src/components/common/Loading/index.tsx b/view-src/components/common/Loading/index.tsx new file mode 100644 index 0000000..1d31075 --- /dev/null +++ b/view-src/components/common/Loading/index.tsx @@ -0,0 +1,23 @@ +import * as React from 'react' + +import 'components/common/Loading/Loading.scss' + +const Loading = (props: any) => { + const color = { + borderTopColor: props.color || '#989898' + } + const size = { + width: (props.size && props.size.width) || '4vw', + height: (props.size && props.size.height) || '4vw' + } + return ( +
+
+
+
+
+
+ ) +} + +export default Loading \ No newline at end of file diff --git a/view-src/images.d.ts b/view-src/images.d.ts new file mode 100644 index 0000000..3f287ea --- /dev/null +++ b/view-src/images.d.ts @@ -0,0 +1,7 @@ +declare module '*.svg' +declare module '*.png' +declare module '*.jpg' +declare module '*.jpeg' +declare module '*.gif' +declare module '*.bmp' +declare module '*.tiff' diff --git a/view-src/images/add.svg b/view-src/images/add.svg new file mode 100644 index 0000000..cabb3a4 --- /dev/null +++ b/view-src/images/add.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/view-src/images/core.svg b/view-src/images/core.svg new file mode 100644 index 0000000..4ed6753 --- /dev/null +++ b/view-src/images/core.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/view-src/images/hook.svg b/view-src/images/hook.svg new file mode 100644 index 0000000..6b596b5 --- /dev/null +++ b/view-src/images/hook.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/view-src/images/kendryte.svg b/view-src/images/kendryte.svg new file mode 100644 index 0000000..32aa6f3 --- /dev/null +++ b/view-src/images/kendryte.svg @@ -0,0 +1,9 @@ + + + + + + diff --git a/view-src/images/libraries-blue.svg b/view-src/images/libraries-blue.svg new file mode 100644 index 0000000..5b8d6ec --- /dev/null +++ b/view-src/images/libraries-blue.svg @@ -0,0 +1,15 @@ + + + + + + diff --git a/view-src/images/libraries.svg b/view-src/images/libraries.svg new file mode 100644 index 0000000..e7972f2 --- /dev/null +++ b/view-src/images/libraries.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/view-src/images/logo.svg b/view-src/images/logo.svg new file mode 100644 index 0000000..6b60c10 --- /dev/null +++ b/view-src/images/logo.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/view-src/images/network-error.svg b/view-src/images/network-error.svg new file mode 100644 index 0000000..ffeb446 --- /dev/null +++ b/view-src/images/network-error.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/view-src/images/project-blue.svg b/view-src/images/project-blue.svg new file mode 100644 index 0000000..1ffb8fb --- /dev/null +++ b/view-src/images/project-blue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/view-src/images/project.svg b/view-src/images/project.svg new file mode 100644 index 0000000..8a49088 --- /dev/null +++ b/view-src/images/project.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/view-src/images/server-error.svg b/view-src/images/server-error.svg new file mode 100644 index 0000000..e07fd92 --- /dev/null +++ b/view-src/images/server-error.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/view-src/images/tag.svg b/view-src/images/tag.svg new file mode 100644 index 0000000..a46361d --- /dev/null +++ b/view-src/images/tag.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/view-src/index.css b/view-src/index.css new file mode 100644 index 0000000..e64f949 --- /dev/null +++ b/view-src/index.css @@ -0,0 +1,21 @@ +body { + height: 100vh; + margin: 0; + padding: 0; + background-color: #1e1e1e; + font-size: 12px; + font-family: "Chinese Quote", -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + overflow: hidden; +} + +a { + text-decoration: none; +} + +::-webkit-scrollbar-corner { + background-color: #1e1e1e; +} + +/* .vscode-light ::-webkit-scrollbar-corner { + background-color: #fff; +} */ diff --git a/view-src/index.tsx b/view-src/index.tsx new file mode 100644 index 0000000..3018943 --- /dev/null +++ b/view-src/index.tsx @@ -0,0 +1,9 @@ +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import App from './App'; +import './index.css'; + +ReactDOM.render( + , + document.getElementById('root') as HTMLElement +); diff --git a/view-src/react-app-env.d.ts b/view-src/react-app-env.d.ts new file mode 100644 index 0000000..6431bc5 --- /dev/null +++ b/view-src/react-app-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/view-src/tsconfig.json b/view-src/tsconfig.json new file mode 100644 index 0000000..6b403a3 --- /dev/null +++ b/view-src/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "module": "esnext", + "target": "es5", + "lib": ["es6", "dom"], + "sourceMap": true, + "allowJs": true, + "jsx": "react", + "moduleResolution": "node", + "rootDir": ".", + "forceConsistentCasingInFileNames": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noImplicitAny": true, + "importHelpers": true, + "strictNullChecks": true, + "suppressImplicitAnyIndexErrors": true, + "noUnusedLocals": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "paths": { + "*": ["*"] + } + } +} + \ No newline at end of file diff --git a/view-src/utils/filter.ts b/view-src/utils/filter.ts new file mode 100644 index 0000000..85cf604 --- /dev/null +++ b/view-src/utils/filter.ts @@ -0,0 +1,22 @@ +interface PackageData { + [key: string]: any +} +export const filter = (keyword: string, data: PackageData): PackageData => { + const resData: PackageData = {} + // Search keyword in packagename and tags + Object.keys(data).map(key => { + const reg = new RegExp(keyword, 'ig') + if (reg.test(key)) { + resData[key] = data[key] + } else { + for (const tag of data[key].tags) { + if (reg.test(tag)) { + resData[key] = data[key] + break + } + } + } + return key + }) + return resData +} \ No newline at end of file diff --git a/view-src/utils/webviewRequest.ts b/view-src/utils/webviewRequest.ts new file mode 100644 index 0000000..b9d3302 --- /dev/null +++ b/view-src/utils/webviewRequest.ts @@ -0,0 +1,34 @@ +import { vscode } from '../vscode' + +const getNonce = () => { + let text = "" + const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + for (let i = 0; i < 32; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)) + } + return text +} + +export interface Message { + [key: string]: any +} + +export const webviewRequest = (msg: Message): Promise => { + return new Promise((resolve, reject) => { + const handleResponse = (event: MessageEvent) => { + const message = event.data + if (message.symbol === msg.symbol) { + window.removeEventListener('message', handleResponse) + if (message.type === 'error') { + reject(message.error) + return + } + resolve(message.data) + } + } + const nonce = getNonce() + msg.symbol = nonce + vscode.postMessage(msg) + window.addEventListener('message', handleResponse) + }) +} \ No newline at end of file diff --git a/view-src/vscode.js b/view-src/vscode.js new file mode 100644 index 0000000..07135e1 --- /dev/null +++ b/view-src/vscode.js @@ -0,0 +1,2 @@ +// eslint-disable-next-line no-undef +export const vscode = process.env.NODE_ENV === 'development' ? undefined : acquireVsCodeApi() \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..a03f170 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,73 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +//@ts-check + +const path = require('path') + +/**@type {import('webpack').Configuration}*/ +const config = { + target: 'node', // vscode插件运行在Node.js环境中 📖 -> https://webpack.js.org/configuration/node/ + + entry: { + extension: path.resolve(__dirname, 'src/extension.ts'), // 插件的入口文件 📖 -> https://webpack.js.org/configuration/entry-context/ + debugadapter: path.resolve(__dirname, 'src/debugadapter.ts') + }, + output: { + // 打包好的文件储存在'dist'文件夹中 (请参考package.json), 📖 -> https://webpack.js.org/configuration/output/ + path: path.resolve(__dirname, 'build'), + filename: '[name].js', + libraryTarget: 'commonjs2', + devtoolModuleFilenameTemplate: '../[resource-path]' + }, + node: { + __dirname: true, + __filename: true + }, + devtool: 'source-map', + externals: { + "vscode": 'commonjs vscode', // vscode-module是热更新的临时目录,所以要排除掉。 在这里添加其他不应该被webpack打包的文件, 📖 -> https://webpack.js.org/configuration/externals/ + "serialport": true, + "bindings": true, + "debug": true, + "ms": true, + "file-uri-to-path": true, + "7zip-bin": true, + "7zip-bin-wrapper": true, + "source-map-support": true, + "split2": true, + "iconv-lite": true, + "buffer-from": true, + "safer-buffer": true, + "inherits": true, + "util-deprecate": true + }, + resolve: { + // 支持读取TypeScript和JavaScript文件, 📖 -> https://github.com/TypeStrong/ts-loader + extensions: ['.ts', '.js'], + alias: { + '@command': path.resolve(__dirname, 'src/command/'), + '@common': path.resolve(__dirname, 'src/common'), + '@utils': path.resolve(__dirname, 'src/utils/'), + '@debug': path.resolve(__dirname, 'src/debug'), + '@service': path.resolve(__dirname, 'src/service'), + 'typings': path.resolve(__dirname, 'node_modules/@types'), + } + }, + module: { + rules: [ + { + test: /\.ts$/, + exclude: /node_modules/, + use: [ + { + loader: 'ts-loader' + } + ] + } + ] + } +}; +module.exports = config;