From b97bc4fee9320adf780a72773a3b6dcafe720eff Mon Sep 17 00:00:00 2001 From: PandaFeng <80824511+Pandafeng-HHU@users.noreply.github.com> Date: Wed, 26 Aug 2026 02:07:44 +0800 Subject: [PATCH] feat: add RT-Thread thread view provider --- README.md | 1 + src/rtos/rtos-rtthread.ts | 380 +++++++++++++++++++++++++++ src/rtos/rtos.ts | 3 + src/test/suite/rtos-rtthread.test.ts | 159 +++++++++++ 4 files changed, 543 insertions(+) create mode 100644 src/rtos/rtos-rtthread.ts create mode 100644 src/test/suite/rtos-rtthread.test.ts diff --git a/README.md b/README.md index 2bb2610..713e26d 100644 --- a/README.md +++ b/README.md @@ -45,3 +45,4 @@ NOTE: The tab name is in the screenshots is called `XRTOS` so it does not confli | ThreadX | @raphaelmeyer | | RTX5 | @thorstendb-ARM | | eCos | @RallySmith | +| RT-Thread (beta) | @Pandafeng-HHU | diff --git a/src/rtos/rtos-rtthread.ts b/src/rtos/rtos-rtthread.ts new file mode 100644 index 0000000..821beea --- /dev/null +++ b/src/rtos/rtos-rtthread.ts @@ -0,0 +1,380 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import * as vscode from 'vscode'; +import { DebugProtocol } from '@vscode/debugprotocol'; +import * as RTOSCommon from './rtos-common'; + +type ThreadState = + | 'INITIALIZED' + | 'CLOSED' + | 'READY' + | 'RUNNING' + | 'SUSPENDED' + | 'SUSPENDED (KILLABLE)' + | 'SUSPENDED (UNINTERRUPTIBLE)' + | 'UNKNOWN'; + +const ThreadTableItems: { [key: string]: RTOSCommon.DisplayColumnItem } = { + name: { + width: 2, + headerRow1: 'Thread', + headerRow2: 'Name', + }, + address: { + width: 2, + headerRow1: '', + headerRow2: 'Address', + }, + state: { + width: 2, + headerRow1: '', + headerRow2: 'State', + }, + priority: { + width: 1, + headerRow1: 'Priority', + headerRow2: 'Current / Init', + colType: RTOSCommon.ColTypeEnum.colTypeNumeric, + }, + tick: { + width: 1, + headerRow1: 'Time Slice', + headerRow2: 'Remain / Init', + colType: RTOSCommon.ColTypeEnum.colTypeNumeric, + }, + error: { + width: 1, + headerRow1: '', + headerRow2: 'Error', + colType: RTOSCommon.ColTypeEnum.colTypeNumeric, + colGapAfter: 1, + }, + stack: { + width: 4, + headerRow1: 'Stack', + headerRow2: 'Current Usage', + colType: RTOSCommon.ColTypeEnum.colTypePercentage, + }, + stackPeak: { + width: 4, + headerRow1: 'Stack', + headerRow2: 'Peak Usage', + colType: RTOSCommon.ColTypeEnum.colTypePercentage, + }, +}; + +const ThreadTableItemNames = Object.keys(ThreadTableItems); + +/** + * RT-Thread thread view. + * + * Threads are discovered through the kernel object container instead of a + * firmware-side helper table. Member offsets are resolved by the debugger, + * so the provider follows the exact layout described by the ELF debug info. + */ +export class RTOSRTThread extends RTOSCommon.RTOSBase { + private static readonly MAX_THREADS = 256; + private static readonly MAX_STACK_READ = 16 * 1024 * 1024; + + private containerSymbol = ''; + private hasSchedulerContext = true; + private listHead: RTOSCommon.RTOSVarHelperMaybe; + private objectListOffset: RTOSCommon.RTOSVarHelperMaybe; + private currentThread: RTOSCommon.RTOSVarHelperMaybe; + private threads: RTOSCommon.RTOSThreadInfo[] = []; + private timeInfo = ''; + + constructor(public session: vscode.DebugSession) { + super(session, 'RT-Thread'); + } + + public async tryDetect(useFrameId: number): Promise { + this.progStatus = 'stopped'; + try { + if (this.status !== 'none') { + return this; + } + + const modernListHead = await this.getVarIfEmpty( + undefined, + useFrameId, + '&_object_container[0].object_list', + true, + ); + if (modernListHead) { + this.containerSymbol = '_object_container'; + this.listHead = modernListHead; + } else { + const legacyListHead = await this.getVarIfEmpty( + undefined, + useFrameId, + '&rt_object_container[0].object_list', + true, + ); + if (!legacyListHead) { + throw new Error('RT-Thread object container was not found'); + } + this.containerSymbol = 'rt_object_container'; + this.listHead = legacyListHead; + } + + this.objectListOffset = await this.getVarIfEmpty( + this.objectListOffset, + useFrameId, + '(unsigned long)&((struct rt_thread *)0)->parent.list', + false, + ); + + const schedulerContext = await this.getVarIfEmpty( + undefined, + useFrameId, + '&((struct rt_thread *)0)->sched_thread_ctx.stat', + true, + ); + this.hasSchedulerContext = schedulerContext !== null; + + this.currentThread = await this.getVarIfEmpty( + this.currentThread, + useFrameId, + '_cpu.current_thread', + true, + ); + if (!this.currentThread) { + this.currentThread = await this.getVarIfEmpty( + this.currentThread, + useFrameId, + '_cpus[0].current_thread', + true, + ); + } + if (!this.currentThread) { + this.currentThread = await this.getVarIfEmpty( + this.currentThread, + useFrameId, + 'rt_current_thread', + true, + ); + } + + this.status = 'initialized'; + } catch (e) { + if (e instanceof RTOSCommon.ShouldRetry) { + console.error(e.message); + } else { + this.status = 'failed'; + this.failedWhy = e; + console.error('RTOSRTThread.tryDetect() failed: ', e); + } + } + return this; + } + + public async refresh(frameId: number): Promise { + if (this.progStatus !== 'stopped' || !this.listHead || !this.objectListOffset || !this.containerSymbol) { + return; + } + + try { + const headAddress = this.parseNumber(await this.listHead.getValue(frameId)); + const listOffset = this.parseNumber(await this.objectListOffset.getValue(frameId)); + const container = `${this.containerSymbol}[0]`; + let nodeAddress = this.parseNumber(await this.getExprVal(`${container}.object_list.next`, frameId)); + const currentAddress = this.currentThread + ? this.parseNumber(await this.currentThread.getValue(frameId)) + : undefined; + + if (headAddress === undefined || listOffset === undefined || nodeAddress === undefined) { + throw new Error('Unable to read the RT-Thread object list'); + } + + const found: RTOSCommon.RTOSThreadInfo[] = []; + const visited = new Set(); + while ( + nodeAddress !== headAddress && + nodeAddress !== 0 && + !visited.has(nodeAddress) && + found.length < RTOSRTThread.MAX_THREADS + ) { + visited.add(nodeAddress); + const threadAddress = nodeAddress - listOffset; + found.push(await this.readThread(threadAddress, currentAddress, frameId)); + + nodeAddress = this.parseNumber( + await this.getExprVal( + `((struct rt_thread *)${RTOSCommon.hexFormat(threadAddress)})->parent.list.next`, + frameId, + ), + ); + if (nodeAddress === undefined) { + throw new Error('Unable to read the next RT-Thread object list node'); + } + } + + this.threads = found; + this.timeInfo = new Date().toLocaleTimeString(); + } catch (e) { + console.error('RTOSRTThread.refresh() failed: ', e); + } + } + + public getHTML(): RTOSCommon.HtmlInfo { + if (this.threads.length === 0) { + return { + html: ` +
+
RT-Thread threads not found
+
+ No thread objects are currently present. The kernel may not have reached + rtthread_startup() yet. +
+
`, + css: '', + }; + } + return this.getHTMLThreads(ThreadTableItemNames, ThreadTableItems, this.threads, this.timeInfo); + } + + private async readThread( + threadAddress: number, + currentAddress: number | undefined, + frameId: number, + ): Promise { + const ptr = `((struct rt_thread *)${RTOSCommon.hexFormat(threadAddress)})`; + const scheduler = this.hasSchedulerContext ? 'sched_thread_ctx.' : ''; + const schedulerPrivate = this.hasSchedulerContext + ? 'sched_thread_ctx.sched_thread_priv.' + : ''; + + const [nameValue, stateValue, currentPriority, initPriority, remainingTick, initTick, errorValue] = + await Promise.all([ + this.getExprVal(`(char *)&${ptr}->parent.name`, frameId), + this.getExprVal(`(unsigned int)${ptr}->${scheduler}stat`, frameId), + this.getExprVal(`(unsigned int)${ptr}->${schedulerPrivate}current_priority`, frameId), + this.getExprVal(`(unsigned int)${ptr}->${schedulerPrivate}init_priority`, frameId), + this.getExprVal(`(unsigned long)${ptr}->${schedulerPrivate}remaining_tick`, frameId), + this.getExprVal(`(unsigned long)${ptr}->${schedulerPrivate}init_tick`, frameId), + this.getExprVal(`(long)${ptr}->error`, frameId), + ]); + + const rawState = this.parseNumber(stateValue) ?? 0xff; + const running = currentAddress !== undefined + ? threadAddress === currentAddress + : (rawState & 0x07) === 0x03; + const stackInfo = await this.readStackInfo(ptr, frameId); + + const stackUsage = this.formatStackUsage(stackInfo.stackUsed, stackInfo.stackSize); + const stackPeak = RTOSCommon.RTOSBase.disableStackPeaks + ? { text: '----', value: undefined } + : this.formatStackUsage(stackInfo.stackPeak, stackInfo.stackSize); + + return { + display: { + name: { text: this.stringFromGdb(nameValue) }, + address: { text: RTOSCommon.hexFormat(threadAddress) }, + state: { text: this.threadState(rawState) }, + priority: { text: `${currentPriority ?? '?'} / ${initPriority ?? '?'}` }, + tick: { text: `${remainingTick ?? '?'} / ${initTick ?? '?'}` }, + error: { text: errorValue ?? '?' }, + stack: stackUsage, + stackPeak, + }, + stackInfo, + running, + }; + } + + private async readStackInfo(ptr: string, frameId: number): Promise { + const [stackAddressValue, stackSizeValue, stackPointerValue] = await Promise.all([ + this.getExprVal(`(unsigned long)${ptr}->stack_addr`, frameId), + this.getExprVal(`(unsigned long)${ptr}->stack_size`, frameId), + this.getExprVal(`(unsigned long)${ptr}->sp`, frameId), + ]); + + const stackStart = this.parseNumber(stackAddressValue) ?? 0; + const stackSize = this.parseNumber(stackSizeValue); + const stackTop = this.parseNumber(stackPointerValue); + const stackInfo: RTOSCommon.RTOSStackInfo = { stackStart, stackTop, stackSize }; + + if (stackSize === undefined || stackSize <= 0) { + return stackInfo; + } + + stackInfo.stackEnd = stackStart + stackSize; + if (stackTop !== undefined) { + stackInfo.stackUsed = Math.max(0, Math.min(stackSize, stackInfo.stackEnd - stackTop)); + stackInfo.stackFree = stackSize - stackInfo.stackUsed; + } + + if (!RTOSCommon.RTOSBase.disableStackPeaks && stackSize <= RTOSRTThread.MAX_STACK_READ) { + try { + const memArg: DebugProtocol.ReadMemoryArguments = { + memoryReference: RTOSCommon.hexFormat(stackStart), + count: stackSize, + }; + const stackData = await this.session.customRequest('readMemory', memArg); + const buf = Buffer.from(stackData.data, 'base64'); + if (buf.length !== stackSize || (stackData.unreadableBytes ?? 0) !== 0) { + return stackInfo; + } + stackInfo.bytes = new Uint8Array(buf); + + let unusedBytes = 0; + while (unusedBytes < stackInfo.bytes.length && stackInfo.bytes[unusedBytes] === 0x23) { + unusedBytes++; + } + stackInfo.stackPeak = stackSize - unusedBytes; + } catch (e) { + console.log('RTOSRTThread: stack peak read failed', e); + } + } + + return stackInfo; + } + + private formatStackUsage(used: number | undefined, size: number | undefined): RTOSCommon.DisplayRowItem { + if (used === undefined || size === undefined || size <= 0) { + return { text: '?', value: undefined }; + } + const percent = Math.max(0, Math.min(100, Math.round((used / size) * 100))); + return { text: `${percent} % (${used} / ${size})`, value: percent }; + } + + private threadState(rawState: number): ThreadState { + switch (rawState & 0x07) { + case 0x00: + return 'INITIALIZED'; + case 0x01: + return 'CLOSED'; + case 0x02: + return 'READY'; + case 0x03: + return 'RUNNING'; + case 0x04: + return 'SUSPENDED'; + case 0x06: + return 'SUSPENDED (KILLABLE)'; + case 0x07: + return 'SUSPENDED (UNINTERRUPTIBLE)'; + default: + return 'UNKNOWN'; + } + } + + private parseNumber(value: string | undefined): number | undefined { + if (!value) { + return undefined; + } + const hex = value.match(/-?0x[0-9a-f]+/i)?.[0]; + if (hex) { + return Number.parseInt(hex, 16); + } + const decimal = value.match(/-?\d+/)?.[0]; + return decimal === undefined ? undefined : Number.parseInt(decimal, 10); + } + + private stringFromGdb(value: string | undefined): string { + if (!value) { + return '?'; + } + return value.match(/"((?:\\.|[^"\\])*)"/)?.[1] ?? '?'; + } +} diff --git a/src/rtos/rtos.ts b/src/rtos/rtos.ts index 82ff80c..225d2ef 100644 --- a/src/rtos/rtos.ts +++ b/src/rtos/rtos.ts @@ -12,6 +12,7 @@ import { RTOSZEPHYR } from './rtos-zephyr'; import { RTOSThreadX } from './rtos-threadx'; import { RTOSRTX5 } from './rtos-rtx5'; import { RTOSeCos } from './rtos-ecos'; +import { RTOSRTThread } from './rtos-rtthread'; import { IDebugTracker, @@ -58,6 +59,8 @@ const RTOS_TYPES = { RTX5: RTOSRTX5, // eslint-disable-next-line @typescript-eslint/naming-convention eCos: RTOSeCos, + // eslint-disable-next-line @typescript-eslint/naming-convention + 'RT-Thread': RTOSRTThread, }; const defaultHtmlInfo: RTOSCommon.HtmlInfo = { html: '', css: '' }; diff --git a/src/test/suite/rtos-rtthread.test.ts b/src/test/suite/rtos-rtthread.test.ts new file mode 100644 index 0000000..470e7b5 --- /dev/null +++ b/src/test/suite/rtos-rtthread.test.ts @@ -0,0 +1,159 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import * as assert from 'assert'; +import * as vscode from 'vscode'; +import { RTOSBase } from '../../rtos/rtos-common'; +import { RTOSRTThread } from '../../rtos/rtos-rtthread'; + +interface FakeThread { + address: number; + nextNode: number; + name: string; + state: number; + currentPriority: number; + initPriority: number; + remainingTick: number; + initTick: number; + error: number; + stackAddress: number; + stackSize: number; + stackPointer: number; + unusedStack: number; +} + +suite('RT-Thread provider', () => { + test('detects RT-Thread and renders its object-list threads', async () => { + const headAddress = 0x1000; + const listOffset = 0x14; + const threads: FakeThread[] = [ + { + address: 0x2000, + nextNode: 0x2114, + name: 'main', + state: 0x03, + currentPriority: 10, + initPriority: 10, + remainingTick: 5, + initTick: 10, + error: 0, + stackAddress: 0x3000, + stackSize: 32, + stackPointer: 0x3018, + unusedStack: 20, + }, + { + address: 0x2100, + nextNode: headAddress, + name: 'worker', + state: 0x04, + currentPriority: 12, + initPriority: 12, + remainingTick: 4, + initTick: 10, + error: -2, + stackAddress: 0x3100, + stackSize: 32, + stackPointer: 0x3110, + unusedStack: 10, + }, + ]; + + const evaluate = (expression: string): string => { + const fixedValues: { [key: string]: string } = { + '&_object_container[0].object_list': hex(headAddress), + '(unsigned long)&((struct rt_thread *)0)->parent.list': hex(listOffset), + '&((struct rt_thread *)0)->sched_thread_ctx.stat': '0x3c', + '_cpu.current_thread': hex(threads[0].address), + '_object_container[0].object_list.next': hex(threads[0].address + listOffset), + }; + if (expression in fixedValues) { + return fixedValues[expression]; + } + if (expression === 'rt_object_container[0]' || expression.startsWith('_cpus[')) { + throw new Error(`No symbol ${expression}`); + } + + const addressText = expression.match(/struct rt_thread \*\)(0x[0-9a-f]+)/i)?.[1]; + const thread = threads.find((item) => item.address === Number.parseInt(addressText ?? '', 16)); + if (!thread) { + throw new Error(`Unexpected expression: ${expression}`); + } + + if (expression.endsWith('->parent.list.next')) { + return hex(thread.nextNode); + } + if (expression.endsWith('->parent.name')) { + return `${hex(thread.address)} "${thread.name}"`; + } + if (expression.endsWith('->sched_thread_ctx.stat')) { + return thread.state.toString(); + } + if (expression.endsWith('->sched_thread_ctx.sched_thread_priv.current_priority')) { + return thread.currentPriority.toString(); + } + if (expression.endsWith('->sched_thread_ctx.sched_thread_priv.init_priority')) { + return thread.initPriority.toString(); + } + if (expression.endsWith('->sched_thread_ctx.sched_thread_priv.remaining_tick')) { + return thread.remainingTick.toString(); + } + if (expression.endsWith('->sched_thread_ctx.sched_thread_priv.init_tick')) { + return thread.initTick.toString(); + } + if (expression.endsWith('->error')) { + return thread.error.toString(); + } + if (expression.endsWith('->stack_addr')) { + return hex(thread.stackAddress); + } + if (expression.endsWith('->stack_size')) { + return thread.stackSize.toString(); + } + if (expression.endsWith('->sp')) { + return hex(thread.stackPointer); + } + throw new Error(`Unexpected expression: ${expression}`); + }; + + const fakeSession = { + id: 'rtthread-test', + name: 'RT-Thread test', + type: 'cortex-debug', + customRequest: async (command: string, args: any): Promise => { + if (command === 'evaluate') { + return { result: evaluate(args.expression), variablesReference: 1 }; + } + if (command === 'readMemory') { + const address = Number.parseInt(args.memoryReference, 16); + const thread = threads.find((item) => item.stackAddress === address); + if (!thread) { + throw new Error(`Unexpected stack address ${args.memoryReference}`); + } + const bytes = Buffer.alloc(thread.stackSize, 0xa5); + bytes.fill(0x23, 0, thread.unusedStack); + return { address: args.memoryReference, data: bytes.toString('base64') }; + } + throw new Error(`Unexpected request: ${command}`); + }, + } as unknown as vscode.DebugSession; + + RTOSBase.disableStackPeaks = false; + const provider = new RTOSRTThread(fakeSession); + await provider.tryDetect(1); + assert.strictEqual(provider.status, 'initialized'); + + await provider.refresh(1); + const html = provider.getHTML().html; + assert.match(html, /main/); + assert.match(html, /worker/); + assert.match(html, /RUNNING/); + assert.match(html, /SUSPENDED/); + assert.match(html, /25 % \(8 \/ 32\)/); + assert.match(html, /38 % \(12 \/ 32\)/); + assert.match(html, /50 % \(16 \/ 32\)/); + assert.match(html, /69 % \(22 \/ 32\)/); + }); +}); + +function hex(value: number): string { + return `0x${value.toString(16)}`; +}