docs: 更新 Memory Viewer 设计文档,新增双击编辑和写入机制
This commit is contained in:
parent
427d83c9bb
commit
e0274c3cb9
|
|
@ -0,0 +1,391 @@
|
|||
# Memory Viewer 内存视图设计文档
|
||||
|
||||
## 1. 概述
|
||||
|
||||
Memory Viewer 是一个嵌入式面板视图,为用户提供实时内存数据查看、导出、导入和填充功能。作为 VS Code 面板容器中的视图(与问题、输出、终端同级),通过 DAP (Debug Adapter Protocol) 自定义请求与 DSS 调试引擎通信。
|
||||
|
||||
### 1.1 核心能力
|
||||
|
||||
| 功能 | 说明 |
|
||||
|------|------|
|
||||
| 内存读取 | 通过 DAP `readMemory` 从调试目标读取指定地址范围的内存数据 |
|
||||
| 数据展示 | 支持十六进制、有符号/无符号整数、单精度/双精度浮点多种格式 |
|
||||
| 滚动加载 | 向下滚动自动追加数据,向上滚动自动回溯历史数据 |
|
||||
| 内存导出 | 支持 TI HEX / 整数文本 / 长整数文本 / 浮点文本 / COFF / 原始二进制 六种格式 |
|
||||
| 内存导入 | 支持自动检测格式(COFF / TI-TXT / Intel HEX)和原始二进制导入 |
|
||||
| 内存填充 | 用指定值填充连续内存区域 |
|
||||
| 双击编辑 | 双击值单元格可修改内存数据,Enter 确认写入,Esc 取消 |
|
||||
| 自动刷新 | 可配置间隔的定时自动刷新 |
|
||||
|
||||
## 2. 架构
|
||||
|
||||
### 2.1 组件关系
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ VS Code 面板容器 │
|
||||
│ ┌───────────────────────────────────────────────────────┐ │
|
||||
│ │ Memory Viewer (WebviewView) │ │
|
||||
│ │ ┌─────────┐ ┌──────────┐ ┌──────────┐ ┌───────┐ │ │
|
||||
│ │ │ 工具栏 │ │ 数据表格 │ │ 状态栏 │ │ 对话框 │ │ │
|
||||
│ │ │(HTML/JS) │ │(渲染引擎) │ │ │ │ │ │ │
|
||||
│ │ └────┬────┘ └────┬─────┘ └────┬─────┘ └───┬───┘ │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ └────────────┴─────────────┴─────────────┘ │ │
|
||||
│ │ postMessage │ │
|
||||
│ └───────────────────────┬───────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌───────────────────────┴───────────────────────────────┐ │
|
||||
│ │ MemoryViewerProvider (Extension) │ │
|
||||
│ │ - 消息路由与业务逻辑 │ │
|
||||
│ │ - 文件对话框交互 │ │
|
||||
│ │ - 自动刷新定时器管理 │ │
|
||||
│ └───────────────────────┬───────────────────────────────┘ │
|
||||
│ │ customRequest │
|
||||
│ ┌───────────────────────┴───────────────────────────────┐ │
|
||||
│ │ DSS Bridge (Adapter) │ │
|
||||
│ │ - DAP 协议封装 │ │
|
||||
│ │ - stdin/stdout JSON 通信 │ │
|
||||
│ └───────────────────────┬───────────────────────────────┘ │
|
||||
│ │ stdin/stdout │
|
||||
│ ┌───────────────────────┴───────────────────────────────┐ │
|
||||
│ │ dss-wrapper.js (Rhino) │ │
|
||||
│ │ - DSS API 调用 │ │
|
||||
│ │ - 中文路径处理 │ │
|
||||
│ │ - UTF-8 编码转换 │ │
|
||||
│ └───────────────────────┬───────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌───────────────────────┴───────────────────────────────┐ │
|
||||
│ │ DSS Debug Engine (Java) │ │
|
||||
│ │ - Memory.readData / writeData │ │
|
||||
│ │ - Memory.saveData / saveRaw │ │
|
||||
│ │ - Memory.load / loadRaw │ │
|
||||
│ │ - Memory.fill │ │
|
||||
│ └───────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 数据流
|
||||
|
||||
#### 读取流程
|
||||
```
|
||||
Webview.scroll/go → postMessage('read') → Provider._read()
|
||||
→ session.customRequest('readMemory') → Bridge → DSS
|
||||
→ DSS readData() → 返回 base64 → 解码为 byte[]
|
||||
→ postMessage('data') → Webview.render()
|
||||
```
|
||||
|
||||
#### 导出流程
|
||||
```
|
||||
Webview.btnExport → showSaveDialog() → postMessage('export')
|
||||
→ Provider._export() → session.customRequest('exportMemory')
|
||||
→ Bridge → DSS saveData()/saveRaw()
|
||||
→ [中文路径: 写入临时文件 → renameTo/复制到目标路径]
|
||||
```
|
||||
|
||||
#### 导入流程
|
||||
```
|
||||
Webview.btnImport → showOpenDialog() → postMessage('import')
|
||||
→ Provider._import() → session.customRequest('importMemory')
|
||||
→ Bridge → DSS load()/loadRaw()
|
||||
→ [中文路径: 复制到临时文件 → DSS 从临时文件加载 → 删除临时文件]
|
||||
```
|
||||
|
||||
#### 写入流程(双击编辑)
|
||||
```
|
||||
Webview commit() → postMessage('write', { address, hexData, callbackId })
|
||||
→ Provider._write(address, hexData, callbackId)
|
||||
→ Buffer.from(hexData, "hex") → base64 编码
|
||||
→ session.customRequest('writeMemory', { memoryReference, data: base64 })
|
||||
→ adapter.writeMemoryRequest() → bridge.writeMemory()
|
||||
→ DSS writeMemory() → writeDataArray() → 逐字节写入
|
||||
→ postMessage('writeResult', { callbackId, success }) → Webview 刷新
|
||||
```
|
||||
|
||||
## 3. 通信协议
|
||||
|
||||
### 3.1 Webview ↔ Extension(postMessage)
|
||||
|
||||
#### Webview → Extension
|
||||
|
||||
| 命令 | 参数 | 说明 |
|
||||
|------|------|------|
|
||||
| `read` | `address, count, prepend?, append?` | 读取内存 |
|
||||
| `autoRefresh` | `enabled, interval` | 设置自动刷新 |
|
||||
| `export` | `address, length, format` | 导出内存 |
|
||||
| `import` | `address, format` | 导入内存 |
|
||||
| `fill` | `address, length, value` | 填充内存 |
|
||||
| `write` | `address, hexData, callbackId` | 写入单个值(双击编辑) |
|
||||
|
||||
#### Extension → Webview
|
||||
|
||||
| 命令 | 参数 | 说明 |
|
||||
|------|------|------|
|
||||
| `data` | `address, bytes, prepend?, append?` | 内存数据返回 |
|
||||
| `error` | `text` | 错误信息 |
|
||||
| `info` | `text` | 成功信息 |
|
||||
| `loading` | `text` | 操作进行中(带脉冲动画) |
|
||||
| `sessionEnded` | — | 调试会话结束,禁用按钮 |
|
||||
| `sessionActive` | — | 调试会话启动,启用按钮 |
|
||||
| `autoRead` | — | 触发自动读取 |
|
||||
| `autoRefreshTick` | — | 自动刷新定时器触发 |
|
||||
| `writeResult` | `callbackId, success, error?` | 写入结果返回 |
|
||||
|
||||
### 3.2 Extension ↔ DSS(DAP Custom Request)
|
||||
|
||||
| 请求名 | 参数 | 返回 |
|
||||
|--------|------|------|
|
||||
| `readMemory` | `memoryReference, count` | `{ data: base64 }` |
|
||||
| `writeMemory` | `memoryReference, data (base64)` | `{ bytesWritten }` |
|
||||
| `exportMemory` | `address, length, format, filePath` | `{ success, message/error }` |
|
||||
| `importMemory` | `address, filePath, format` | `{ success, message/error }` |
|
||||
| `fillMemory` | `address, length, value` | `{ success, message/error }` |
|
||||
|
||||
## 4. 导出格式
|
||||
|
||||
| 格式标识 | 名称 | DSS API | IOFormat | 文件扩展名 |
|
||||
|----------|------|---------|----------|-----------|
|
||||
| `hex` | TI HEX (TI-DAT) | `saveData` | `IOFormat.HEX (1)` | .txt |
|
||||
| `int` | 整数文本 | `saveData` | `IOFormat.INT (2)` | .txt |
|
||||
| `long` | 长整数文本 | `saveData` | `IOFormat.LONG (3)` | .txt |
|
||||
| `float` | 浮点文本 | `saveData` | `IOFormat.FLOAT (4)` | .txt |
|
||||
| `coff` | COFF | `saveData` | `IOFormat.COFF (5)` | .out |
|
||||
| `raw` | 原始二进制 | `saveRaw` | — | .bin |
|
||||
|
||||
### 4.1 saveRaw 参数说明
|
||||
|
||||
```
|
||||
saveRaw(nPage, nAddress, sFilename, nLength, nTypeSize, bByteSwap)
|
||||
```
|
||||
|
||||
- **nLength**: 单位是 target memory words(C66xx 32 位 = 4 字节),前端传字节数需除以 4
|
||||
- **nTypeSize = 32**: 按 32 位字处理字节序(C66xx 大端 → 主机小端自动转换)
|
||||
- **bByteSwap = false**: 启用自动字节序转换
|
||||
|
||||
## 5. 导入格式
|
||||
|
||||
### 5.1 格式检测策略(优先级从高到低)
|
||||
|
||||
1. 用户显式选择 `raw` 格式 → 使用 `loadRaw` 加载
|
||||
2. 文件扩展名为 `.bin` / `.dat` / `.raw` → 使用 `loadRaw` 加载
|
||||
3. 其他格式 → 使用 `load()` 自动检测(根据文件头识别 COFF / TI-TXT / Intel HEX)
|
||||
|
||||
### 5.2 loadRaw 参数说明
|
||||
|
||||
```
|
||||
loadRaw(nPage, nAddress, sFilename, nTypeSize, bByteSwap)
|
||||
```
|
||||
|
||||
- **nTypeSize = 32**: 与 saveRaw 的 nTypeSize=32 匹配,确保字节序一致
|
||||
- **文件大小**: DSS 自动确定,无需手动指定长度
|
||||
|
||||
## 6. 中文路径处理
|
||||
|
||||
### 6.1 问题根因
|
||||
|
||||
中文 Windows 下,DSS 的 Rhino 引擎和 Java 文件 I/O 存在两层编码问题:
|
||||
|
||||
| 层级 | 问题 | 影响 |
|
||||
|------|------|------|
|
||||
| stdin/stdout 通信 | `InputStreamReader(System.in)` 默认使用 GBK 编码 | JSON 中的中文路径乱码 |
|
||||
| 文件 I/O | `saveData`/`load` 内部用 GBK 编码文件名 | 创建的文件名乱码,或找不到文件 |
|
||||
|
||||
### 6.2 解决方案
|
||||
|
||||
#### stdin/stdout 编码修复
|
||||
|
||||
```javascript
|
||||
// stdin: 指定 UTF-8 编码
|
||||
var reader = new BufferedReader(new InputStreamReader(System.in, "UTF-8"));
|
||||
|
||||
// stdout: 用 _writeUtf8() 直接写 UTF-8 字节
|
||||
function _writeUtf8(msg) {
|
||||
var bytes = new java.lang.String(str).getBytes('UTF-8');
|
||||
System.out['write(byte[])'](bytes);
|
||||
System.out['write(int)'](0x0A);
|
||||
System.out.flush();
|
||||
}
|
||||
```
|
||||
|
||||
#### 文件 I/O 中文路径 workaround
|
||||
|
||||
**导出**: 先写入 ASCII 临时文件 → `File.renameTo` 或手动复制到目标路径
|
||||
|
||||
**导入**: 先复制源文件到 ASCII 临时文件 → DSS API 从临时文件加载 → 删除临时文件
|
||||
|
||||
```javascript
|
||||
// 检测是否需要 workaround
|
||||
var needsWorkaround = /[^\x00-\x7F]/.test(filePath);
|
||||
|
||||
if (needsWorkaround) {
|
||||
var tempFile = new java.io.File(tempDir, "dss_export_" + nanoTime + ".tmp");
|
||||
// ... DSS 写入临时文件 ...
|
||||
// 然后移动到目标路径
|
||||
tempFile.renameTo(targetFile); // 或手动复制
|
||||
}
|
||||
```
|
||||
|
||||
## 7. 前端交互设计
|
||||
|
||||
### 7.1 双击编辑机制
|
||||
|
||||
#### 交互流程
|
||||
|
||||
```
|
||||
用户双击 c-val 单元格
|
||||
→ 创建 input 输入框,显示当前格式下的原始值
|
||||
→ 用户修改值,按 Enter 或失焦触发 commit()
|
||||
→ commit() 处理:
|
||||
1. committed 标志防止 Enter 和 blur 双重触发
|
||||
2. 根据当前格式(hex/uint/int/float/double)将输入值转换为字节数组
|
||||
3. NaN 检查:十六进制输入无效时回退并提示
|
||||
4. hex 格式特殊处理:字节序反转(hex 显示时已反转,编辑时需再反转回来)
|
||||
5. 构造 hexData 字符串
|
||||
6. 通过 postMessage('write') 发送写入请求
|
||||
→ 按 Esc 取消编辑,恢复原始文本
|
||||
```
|
||||
|
||||
#### 值格式转换规则
|
||||
|
||||
| 格式 | 输入示例 | 转换逻辑 | 字节序 |
|
||||
|------|---------|---------|--------|
|
||||
| hex | `000003E8` | 直接解析为字节 | 显示时反转,写入时再反转 |
|
||||
| uint | `1000` | parseInt → 逐字节提取 | 小端序 |
|
||||
| int | `-123` | parseInt → 补码转换 → 逐字节提取 | 小端序 |
|
||||
| float | `3.14` | Float32Array 编码(4 字节) | 小端序 |
|
||||
| double | `3.14159265` | Float64Array 编码(8 字节) | 小端序 |
|
||||
|
||||
#### 数据流
|
||||
|
||||
```
|
||||
commit() → valueToHexBytes(value, format, sizeBytes)
|
||||
→ hexStr = hex字节拼接(hex 格式需反转字节序)
|
||||
→ postMessage('write', { address, hexData, callbackId })
|
||||
→ Provider._write()
|
||||
→ Buffer.from(hexData, "hex") → base64 编码
|
||||
→ session.customRequest('writeMemory', { memoryReference, data: base64 })
|
||||
→ adapter.writeMemoryRequest() → bridge.writeMemory()
|
||||
→ DSS writeMemory() → writeDataArray() → 逐字节写入
|
||||
→ 返回 writeResult { success, callbackId }
|
||||
→ Webview 更新状态栏 + autoRead 刷新
|
||||
```
|
||||
|
||||
### 7.2 callbackId 机制
|
||||
|
||||
双击编辑使用 `callbackId` 匹配异步写入结果:
|
||||
|
||||
```javascript
|
||||
var writeCallbackId = 0;
|
||||
var writeCallbacks = {}; // { [cbId]: { addr, td, origBytes, newHex } }
|
||||
|
||||
// commit() 时:
|
||||
var cbId = ++writeCallbackId;
|
||||
writeCallbacks[cbId] = { addr, td, origBytes, newHex };
|
||||
V.postMessage({ command: 'write', address, hexData, callbackId: cbId });
|
||||
|
||||
// 收到 writeResult 时:
|
||||
var cb = writeCallbacks[m.callbackId];
|
||||
if (cb) {
|
||||
if (m.success) { st.textContent = '写入成功: ' + cb.addr; }
|
||||
else { st.textContent = '写入失败: ' + m.error; cb.td.textContent = cb.origBytes; }
|
||||
delete writeCallbacks[m.callbackId];
|
||||
}
|
||||
```
|
||||
|
||||
### 7.3 按钮状态管理
|
||||
|
||||
| 状态 | 读取/翻页 | 导出/导入/填充 |
|
||||
|------|----------|---------------|
|
||||
| 无调试会话 | 禁用 | 禁用 |
|
||||
| 有调试会话 | 启用 | 启用 |
|
||||
| 操作进行中 | 启用 | 禁用 |
|
||||
|
||||
### 7.4 状态栏反馈
|
||||
|
||||
| 状态 | 样式 | 文本示例 |
|
||||
|------|------|----------|
|
||||
| 就绪 | 默认 | `就绪` |
|
||||
| 加载中 | 默认 | `加载中...` |
|
||||
| 导出中 | 脉冲动画 | `正在导出...` |
|
||||
| 导入中 | 脉冲动画 | `正在导入...` |
|
||||
| 提交修改中 | 脉冲动画 | `正在提交修改...` |
|
||||
| 成功 | 绿色 | `导出成功: test.bin` |
|
||||
| 写入成功 | 绿色 | `写入成功: 0x0C000000` |
|
||||
| 错误 | 红色 | `导入失败: ...` |
|
||||
| 写入失败 | 红色 | `写入失败: ...` |
|
||||
| 会话结束 | 红色 | `会话结束` |
|
||||
|
||||
### 7.5 滚动加载机制
|
||||
|
||||
- **向下滚动**: 当 `scrollTop + clientHeight >= scrollHeight - 2*ROW_H` 时,请求追加下一个 CHUNK(4096 字节)
|
||||
- **向上滚动**: 当 `scrollTop <= 2*ROW_H` 且 `curAddr > 0` 时,请求回溯上一个 CHUNK
|
||||
- **防抖**: `busy` 和 `scrollBusy` 标志防止并发请求
|
||||
|
||||
## 8. writeMemory 写入机制
|
||||
|
||||
### 8.1 调用链路
|
||||
|
||||
```
|
||||
Webview commit()
|
||||
→ postMessage('write', { address, hexData, callbackId })
|
||||
→ Provider._write(address, hexData, callbackId)
|
||||
→ Buffer.from(hexData, "hex") → base64 编码
|
||||
→ session.customRequest('writeMemory', { memoryReference: address, data: base64 })
|
||||
→ adapter.writeMemoryRequest() → bridge.writeMemory(address, hexData)
|
||||
→ DSS writeMemory() → 将 hexData 字符串解析为 byteArray
|
||||
→ writeDataArray() 逐字节调用 writeData(page, addr, value, 8)
|
||||
→ 返回 { success, bytesWritten }
|
||||
→ Provider postMessage('writeResult', { callbackId, success, error? })
|
||||
```
|
||||
|
||||
### 8.2 writeDataArray 实现
|
||||
|
||||
```javascript
|
||||
function writeDataArray(page, address, byteArr, typeSize) {
|
||||
for (var i = 0; i < byteArr.length; i++) {
|
||||
session.memory.writeData(page, address + i, byteArr[i], typeSize);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
使用单值 `writeData(page, address, value, typeSize)` 逐字节写入,避免 Rhino 方法重载歧义(`write(byte[])` vs `write(int)`)。
|
||||
|
||||
### 8.3 错误处理
|
||||
|
||||
`writeMemory` 函数先尝试写入 `PROGRAM` 页面,失败后自动回退到 `DATA` 页面。
|
||||
|
||||
## 9. 安全策略
|
||||
|
||||
### 9.1 Content Security Policy (CSP)
|
||||
|
||||
```html
|
||||
<meta http-equiv="Content-Security-Policy"
|
||||
content="default-src 'none';
|
||||
style-src 'unsafe-inline' 'nonce-${nonce}';
|
||||
script-src 'nonce-${nonce}';">
|
||||
```
|
||||
|
||||
- 所有 `<style>` 和 `<script>` 标签必须携带匹配的 nonce
|
||||
- 禁止外部资源加载
|
||||
- 禁止内联事件处理器(`onclick` 等),使用 `addEventListener` 替代
|
||||
|
||||
## 10. 文件结构
|
||||
|
||||
```
|
||||
packages/dsp-mod-debug/
|
||||
├── src/
|
||||
│ ├── memoryViewer.ts # WebviewViewProvider 实现 + 前端 HTML/JS
|
||||
│ ├── extension.ts # 注册视图、监听调试会话事件
|
||||
│ └── dssAdapter/
|
||||
│ ├── adapter.ts # DAP 适配器,处理自定义请求路由
|
||||
│ └── dssBridge.ts # DSS 进程通信桥接
|
||||
└── dss_script/
|
||||
└── dss-wrapper.js # DSS Rhino 脚本,封装底层调试 API
|
||||
```
|
||||
|
||||
## 11. 已知限制
|
||||
|
||||
1. **DSS API 限制**: `Memory.IOFormat` 枚举不包含 Intel HEX 格式,导出的 "Hex" 实际为 TI HEX 格式
|
||||
2. **saveRaw nLength 单位**: DSS 的 `saveRaw` 长度参数单位是 target memory words(32 位),非字节数
|
||||
3. **编码 workaround**: 中文路径依赖临时文件中转,增加了一次文件复制开销
|
||||
4. **Rhino 兼容性**: Java 方法重载(如 `write(byte[])` vs `write(int)`)需要使用 `['method签名']` 显式指定
|
||||
|
|
@ -3997,12 +3997,7 @@ function writeMemory(address, data) {
|
|||
return { success: false, error: 'Target is not connected' };
|
||||
}
|
||||
|
||||
/**
|
||||
* 向目标内存写入数据
|
||||
*
|
||||
* DSS API: session.memory.writeData(page, address, longValues, typeSize)
|
||||
*/
|
||||
print("[DSS] Writing memory at address " + address);
|
||||
print("[DSS] writeMemory called: address=" + address + ", dataType=" + typeof data + ", data=" + data);
|
||||
|
||||
// 将十六进制地址字符串转换为数字
|
||||
var numAddress = parseInt(address, 16);
|
||||
|
|
@ -4298,9 +4293,11 @@ function toJavaLongArray(arr) {
|
|||
* @param {Array} byteArr - 字节数组
|
||||
*/
|
||||
function writeDataArray(page, address, byteArr, typeSize) {
|
||||
print("[DSS] writeDataArray: page=" + page + ", addr=0x" + address.toString(16) + ", len=" + byteArr.length + ", typeSize=" + typeSize);
|
||||
for (var i = 0; i < byteArr.length; i++) {
|
||||
session.memory.writeData(page, address + i, byteArr[i], typeSize);
|
||||
}
|
||||
print("[DSS] writeDataArray: completed writing " + byteArr.length + " bytes");
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -235,6 +235,8 @@ export class DSSDebugSession extends LoggingDebugSession {
|
|||
supportsEvaluateForHovers: true,
|
||||
// 支持内存读取请求
|
||||
supportsReadMemoryRequest: true,
|
||||
// 支持内存写入请求
|
||||
supportsWriteMemoryRequest: true,
|
||||
// 不支持指定步入目标
|
||||
supportsStepInTargetsRequest: false,
|
||||
// 不支持代码补全
|
||||
|
|
@ -1135,6 +1137,42 @@ export class DSSDebugSession extends LoggingDebugSession {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 内存写入请求处理(标准 DAP writeMemory)
|
||||
*
|
||||
* 当通过 session.customRequest("writeMemory", ...) 调用时,
|
||||
* VS Code 会路由到这个标准 DAP 处理器。
|
||||
*
|
||||
* @param response 内存写入响应对象
|
||||
* @param args 参数(包含内存地址和 base64 编码的数据)
|
||||
*/
|
||||
protected async writeMemoryRequest(
|
||||
response: DebugProtocol.WriteMemoryResponse,
|
||||
args: DebugProtocol.WriteMemoryArguments
|
||||
) {
|
||||
try {
|
||||
if (!this.bridge) {
|
||||
this.sendErrorResponse(response, -1, "Bridge not initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
const address = args.memoryReference;
|
||||
const data = args.data; // base64 encoded
|
||||
const bytes = Buffer.from(data, "base64");
|
||||
const hexData = bytes.toString("hex");
|
||||
|
||||
this.debug(`[DSS-DAP] writeMemoryRequest: address=${address}, bytes=${bytes.length}`);
|
||||
|
||||
await this.bridge.writeMemory(address, hexData);
|
||||
|
||||
response.body = { bytesWritten: bytes.length };
|
||||
this.sendResponse(response);
|
||||
} catch (err) {
|
||||
const error = err instanceof Error ? err.message : String(err);
|
||||
this.sendErrorResponse(response, -1, `Failed to write memory: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重启请求处理
|
||||
*
|
||||
|
|
@ -1215,6 +1253,8 @@ export class DSSDebugSession extends LoggingDebugSession {
|
|||
return;
|
||||
}
|
||||
|
||||
// writeMemory 已由标准 DAP writeMemoryRequest 处理,不再需要在此处理
|
||||
|
||||
// 未知命令
|
||||
this.sendErrorResponse(response, -1, `Unknown custom request: ${command}`);
|
||||
} catch (err) {
|
||||
|
|
|
|||
|
|
@ -82,10 +82,15 @@ export class MemoryViewerProvider implements vscode.WebviewViewProvider {
|
|||
view.webview.html = this._getHtml(view.webview);
|
||||
|
||||
view.webview.onDidReceiveMessage(async (msg) => {
|
||||
console.log("[MemoryViewer] Received message from webview:", msg.command);
|
||||
switch (msg.command) {
|
||||
case "read":
|
||||
await this._read(msg.address, msg.count, msg.prepend, msg.append);
|
||||
break;
|
||||
case "write":
|
||||
console.log("[MemoryViewer] Write command received:", msg);
|
||||
await this._write(msg.address, msg.hexData, msg.callbackId);
|
||||
break;
|
||||
case "autoRefresh":
|
||||
this._setAutoRefresh(msg.enabled, msg.interval);
|
||||
break;
|
||||
|
|
@ -273,6 +278,38 @@ export class MemoryViewerProvider implements vscode.WebviewViewProvider {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 向目标内存写入数据。
|
||||
*
|
||||
* 通过 DAP 自定义请求 `writeMemory` 将十六进制字节数据写入指定地址。
|
||||
* 写入成功后自动刷新显示。
|
||||
*
|
||||
* @param address - 目标起始地址(十六进制字符串)
|
||||
* @param hexData - 要写入的数据(十六进制字符串,如 "0A1B2C3D")
|
||||
* @param callbackId - webview 回调标识,用于通知写入结果
|
||||
*/
|
||||
private async _write(address: string, hexData: string, callbackId: number): Promise<void> {
|
||||
console.log(`[MemoryViewer] _write called: address=${address}, hexData=${hexData}, callbackId=${callbackId}`);
|
||||
const session = vscode.debug.activeDebugSession;
|
||||
if (!session) {
|
||||
console.log("[MemoryViewer] _write failed: no active debug session");
|
||||
this._view?.webview.postMessage({ command: "writeResult", callbackId, success: false, error: "没有活动的调试会话" });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// 将 hex 字符串转换为 base64(DAP writeMemory 标准格式)
|
||||
const bytes = Buffer.from(hexData, "hex");
|
||||
const base64Data = bytes.toString("base64");
|
||||
await session.customRequest("writeMemory", { memoryReference: address, data: base64Data });
|
||||
this._view?.webview.postMessage({ command: "writeResult", callbackId, success: true });
|
||||
this._view?.webview.postMessage({ command: "autoRead" });
|
||||
} catch (e) {
|
||||
const errMsg = e instanceof Error ? e.message : String(e);
|
||||
console.log("[MemoryViewer] _write failed:", errMsg);
|
||||
this._view?.webview.postMessage({ command: "writeResult", callbackId, success: false, error: errMsg });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 用指定值填充内存区域。
|
||||
*
|
||||
|
|
@ -334,7 +371,9 @@ body{font-family:var(--vscode-font-family);font-size:12px;color:var(--vscode-for
|
|||
table{border-collapse:collapse;white-space:nowrap}
|
||||
td{padding:0 3px;vertical-align:top}
|
||||
.c-addr{color:var(--vscode-debugTokenExpression-name);text-align:right;padding-right:6px;border-right:1px solid var(--vscode-editorWidget-border);user-select:none}
|
||||
.c-val{color:var(--vscode-debugTokenExpression-number);letter-spacing:.5px}
|
||||
.c-val-row{display:flex;gap:4px;flex-wrap:wrap}
|
||||
.c-val{color:var(--vscode-debugTokenExpression-number);letter-spacing:.5px;padding:0 2px;border-radius:2px;cursor:pointer}
|
||||
.c-val:hover{background:var(--vscode-list-hoverBackground)}
|
||||
.c-sep{color:var(--vscode-descriptionForeground);padding:0 2px;user-select:none}
|
||||
.bar2{font-size:10px;color:var(--vscode-descriptionForeground);display:flex;justify-content:space-between;flex-shrink:0;padding-top:4px;border-top:1px solid var(--vscode-editorWidget-border)}
|
||||
.err{color:var(--vscode-errorForeground)}
|
||||
|
|
@ -439,12 +478,14 @@ tr:hover{background:var(--vscode-list-hoverBackground)}
|
|||
<script nonce="${n}">
|
||||
(function(){
|
||||
var V=acquireVsCodeApi();
|
||||
console.log('[MemoryViewer] VS Code API acquired:', typeof V, typeof V.postMessage);
|
||||
var $=id=>document.getElementById(id);
|
||||
var tb=$('tb'),tbl=$('tbl'),addr=$('addr'),st=$('st'),rg=$('rg'),sz=$('sz'),col=$('col'),fmt=$('fmt');
|
||||
|
||||
var curAddr=0,busy=false,scrollBusy=false,opBusy=false;
|
||||
var CHUNK=4096,ROW_H=18;
|
||||
var btns=['btnExport','btnImport','btnFill','go','pu','pd'];
|
||||
var writeCallbackId=0,writeCallbacks={};
|
||||
|
||||
function setSessionActive(on){
|
||||
btns.forEach(function(id){var b=$(id);if(b)b.disabled=!on;});
|
||||
|
|
@ -528,10 +569,13 @@ function render(base,bytes){
|
|||
var bpr=col.value*sz.value;var ds=parseInt(sz.value);var fv=fmt.value;var h='';
|
||||
for(var i=0;i<bytes.length;i+=bpr){
|
||||
var a=base+i,row=bytes.slice(i,i+bpr);
|
||||
h+='<tr><td class="c-addr">'+fmtAddr(a)+'</td><td class="c-sep">:</td>';
|
||||
var vp='';
|
||||
for(var g=0;g<row.length;g+=ds){var grp=row.slice(g,g+ds);vp+=formatValue(grp,fv);if(g+ds<row.length)vp+=' ';}
|
||||
h+='<td class="c-val">'+vp+'</td></tr>';
|
||||
h+='<tr><td class="c-addr">'+fmtAddr(a)+'</td><td class="c-sep">:</td><td class="c-val-row">';
|
||||
for(var g=0;g<row.length;g+=ds){
|
||||
var grp=row.slice(g,g+ds);
|
||||
var grpAddr=a+g;
|
||||
h+='<span class="c-val" data-addr="0x'+grpAddr.toString(16)+'" data-bytes="'+grp.map(fmtHex).join('')+'">'+formatValue(grp,fv)+'</span>';
|
||||
}
|
||||
h+='</td></tr>';
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
|
@ -560,6 +604,97 @@ addr.onkeydown=function(e){if(e.key==='Enter')doRead()};
|
|||
$('ar').onchange=function(){V.postMessage({command:'autoRefresh',enabled:$('ar').checked,interval:parseInt($('ari').value)||1000})};
|
||||
sz.onchange=doRead;col.onchange=doRead;fmt.onchange=doRead;
|
||||
|
||||
// === 双击编辑值 ===
|
||||
function valueToHexBytes(val,fv,szBytes){
|
||||
var bs=[];
|
||||
switch(fv){
|
||||
case 'hex':
|
||||
var s=val.replace(/^0x/i,'');if(s.length%2)s='0'+s;
|
||||
for(var i=0;i<s.length;i+=2)bs.push(parseInt(s.substr(i,2),16));
|
||||
break;
|
||||
case 'uint':
|
||||
var n=parseInt(val,10);if(isNaN(n))return null;
|
||||
for(var i=0;i<szBytes;i++){bs.push(n&0xFF);n=n>>>8;}
|
||||
break;
|
||||
case 'int':
|
||||
var n=parseInt(val,10);if(isNaN(n))return null;
|
||||
if(n<0)n=n+Math.pow(2,szBytes*8);
|
||||
for(var i=0;i<szBytes;i++){bs.push(n&0xFF);n=n>>>8;}
|
||||
break;
|
||||
case 'float':
|
||||
var f=parseFloat(val);if(isNaN(f))return null;
|
||||
var ab=new ArrayBuffer(4);new Float32Array(ab)[0]=f;
|
||||
var u8=new Uint8Array(ab);for(var i=0;i<4;i++)bs.push(u8[i]);
|
||||
break;
|
||||
case 'double':
|
||||
var f=parseFloat(val);if(isNaN(f))return null;
|
||||
var ab=new ArrayBuffer(8);new Float64Array(ab)[0]=f;
|
||||
var u8=new Uint8Array(ab);for(var i=0;i<8;i++)bs.push(u8[i]);
|
||||
break;
|
||||
default:return null;
|
||||
}
|
||||
return bs;
|
||||
}
|
||||
|
||||
tbl.addEventListener('dblclick',function(e){
|
||||
var td=e.target;if(!td||td.className!=='c-val')return;
|
||||
var addrStr=td.getAttribute('data-addr');if(!addrStr)return;
|
||||
var origBytes=td.getAttribute('data-bytes');if(!origBytes)return;
|
||||
var szBytes=parseInt(sz.value);var fv=fmt.value;
|
||||
var firstGroup=origBytes.split(' ')[0];
|
||||
var inputVal='';
|
||||
switch(fv){
|
||||
case 'hex':inputVal=firstGroup.match(/.{2}/g).reverse().join('');break;
|
||||
case 'uint':inputVal=''+bytesToUint(firstGroup.match(/.{2}/g).map(function(h){return parseInt(h,16)}),false);break;
|
||||
case 'int':inputVal=''+bytesToInt(firstGroup.match(/.{2}/g).map(function(h){return parseInt(h,16)}),false);break;
|
||||
case 'float':inputVal=''+bytesToFloat(firstGroup.match(/.{2}/g).map(function(h){return parseInt(h,16)}));break;
|
||||
case 'double':inputVal=''+bytesToDouble(firstGroup.match(/.{2}/g).map(function(h){return parseInt(h,16)}));break;
|
||||
}
|
||||
var origText=td.textContent;
|
||||
var input=document.createElement('input');
|
||||
input.type='text';input.value=inputVal;
|
||||
input.style.cssText='width:100%;background:var(--vscode-input-background);color:var(--vscode-input-foreground);border:1px solid var(--vscode-focusBorder);border-radius:2px;outline:none;padding:0 2px;font:inherit;';
|
||||
td.textContent='';td.appendChild(input);input.focus();input.select();
|
||||
|
||||
var committed=false;
|
||||
function commit(){
|
||||
if(committed)return;committed=true;
|
||||
st.textContent='正在提交修改...';st.className='loading';
|
||||
var newVal=input.value.trim();
|
||||
var hexBytes=valueToHexBytes(newVal,fv,szBytes);
|
||||
console.log('[MemoryViewer] commit() called, newVal:', newVal, 'hexBytes:', hexBytes);
|
||||
if(!hexBytes||hexBytes.length===0){
|
||||
td.textContent=origText;
|
||||
st.textContent='无效的输入值';st.className='err';
|
||||
console.log('[MemoryViewer] commit() failed: empty hexBytes');
|
||||
return;
|
||||
}
|
||||
// 检查是否包含 NaN(无效的十六进制输入)
|
||||
for(var i=0;i<hexBytes.length;i++){
|
||||
if(isNaN(hexBytes[i])){
|
||||
td.textContent=origText;
|
||||
st.textContent='无效的十六进制值';st.className='err';
|
||||
console.log('[MemoryViewer] commit() failed: NaN in hexBytes');
|
||||
return;
|
||||
}
|
||||
}
|
||||
td.textContent=origText;
|
||||
if(fv==='hex')hexBytes.reverse();
|
||||
var hexStr='';for(var i=0;i<hexBytes.length;i++)hexStr+=fmtHex(hexBytes[i]);
|
||||
var cbId=++writeCallbackId;
|
||||
writeCallbacks[cbId]={addr:addrStr,td:td,origBytes:firstGroup,newHex:hexStr};
|
||||
st.textContent='发送写入命令: '+addrStr+' = '+hexStr;st.className='';
|
||||
console.log('[MemoryViewer] commit() sending postMessage:', {command:'write',address:addrStr,hexData:hexStr,callbackId:cbId});
|
||||
V.postMessage({command:'write',address:addrStr,hexData:hexStr,callbackId:cbId});
|
||||
console.log('[MemoryViewer] commit() postMessage sent');
|
||||
}
|
||||
input.onblur=function(){commit();};
|
||||
input.onkeydown=function(ev){
|
||||
if(ev.key==='Enter'){ev.preventDefault();commit();}
|
||||
if(ev.key==='Escape'){committed=true;td.textContent=origText;}
|
||||
};
|
||||
});
|
||||
|
||||
new ResizeObserver(function(){updateSize();doRead()}).observe(document.body);
|
||||
|
||||
// === 对话框:导出 ===
|
||||
|
|
@ -613,6 +748,14 @@ window.addEventListener('message',function(e){
|
|||
}else if(m.command==='error'){st.textContent=m.text;st.className='err';busy=false;scrollBusy=false;setOpBusy(false);}
|
||||
else if(m.command==='loading'){st.textContent=m.text+'...';st.className='loading';setOpBusy(true);}
|
||||
else if(m.command==='info'){st.textContent=m.text;st.className='info';setOpBusy(false);}
|
||||
else if(m.command==='writeResult'){
|
||||
var cb=writeCallbacks[m.callbackId];
|
||||
if(cb){
|
||||
if(m.success){st.textContent='写入成功: '+cb.addr;st.className='info';}
|
||||
else{st.textContent='写入失败: '+m.error;st.className='err';cb.td.textContent=cb.origBytes;}
|
||||
delete writeCallbacks[m.callbackId];
|
||||
}
|
||||
}
|
||||
else if(m.command==='sessionEnded'){$('ar').checked=false;st.textContent='会话结束';st.className='err';setSessionActive(false);setOpBusy(false);}
|
||||
else if(m.command==='sessionActive'){setSessionActive(true);st.textContent='就绪';st.className='';}
|
||||
else if(m.command==='autoRead'||m.command==='autoRefreshTick'){doRead();}
|
||||
|
|
|
|||
Loading…
Reference in New Issue