rtos-views
Version:
RTOS views for microcontrollers
611 lines (559 loc) • 23.5 kB
text/typescript
import * as vscode from 'vscode';
import * as os from 'os';
import * as fs from 'fs';
import * as path from 'path';
import * as RTOSCommon from './rtos-common';
import { RTOSFreeRTOS } from './rtos-freertos';
import { RTOSUCOS2 } from './rtos-ucosii';
import { RTOSEmbOS } from './rtos-embos';
import { RTOSChibiOS } from './rtos-chibios';
import { RTOSZEPHYR } from './rtos-zephyr';
import {
IDebugTracker,
IDebuggerTrackerSubscribeArg,
IDebuggerTrackerEvent,
IDebuggerSubscription,
OtherDebugEvents,
DebugSessionStatus,
DebugTracker,
} from 'debug-tracker-vscode';
// eslint-disable-next-line @typescript-eslint/naming-convention
export const TrackedDebuggers = [
'cortex-debug',
'cppdbg', // Microsoft debugger
'cspy', // IAR debugger
];
let trackerApi: IDebugTracker;
let trackerApiClientInfo: IDebuggerSubscription;
const RTOS_TYPES = {
// eslint-disable-next-line @typescript-eslint/naming-convention
FreeRTOS: RTOSFreeRTOS,
// eslint-disable-next-line @typescript-eslint/naming-convention
'uC/OS-II': RTOSUCOS2,
embOS: RTOSEmbOS,
// eslint-disable-next-line @typescript-eslint/naming-convention
ChibiOS: RTOSChibiOS,
// eslint-disable-next-line @typescript-eslint/naming-convention
Zephyr: RTOSZEPHYR,
};
const defaultHtmlInfo: RTOSCommon.HtmlInfo = { html: '', css: '' };
export class RTOSSession {
public lastFrameId: number | undefined;
public htmlContent: RTOSCommon.HtmlInfo = defaultHtmlInfo;
public rtos: RTOSCommon.RTOSBase | undefined; // The final RTOS
private allRTOSes: RTOSCommon.RTOSBase[] = [];
public triedAndFailed = false;
constructor(public session: vscode.DebugSession) {
this.lastFrameId = undefined;
for (const rtosType of Object.values(RTOS_TYPES)) {
this.allRTOSes.push(new rtosType(session));
}
}
// This is the work horse. Do not call it if the panel is in disabled state.
public async onStopped(frameId: number): Promise<void> {
return new Promise<void>((resolve) => {
this.lastFrameId = frameId;
const doRefresh = () => {
if (this.rtos) {
this.htmlContent.html =
'<p>RTOS Views: Failed to get RTOS information. Please report an issue if RTOS is actually running</p>\n';
this.htmlContent.css = '';
this.rtos.onStopped(frameId).then(() => {
this.htmlContent = this.rtos?.getHTML() || defaultHtmlInfo;
resolve();
});
} else {
this.triedAndFailed = true;
this.htmlContent.html = '';
this.htmlContent.css = '';
resolve();
}
};
if (this.rtos === undefined && this.allRTOSes.length > 0) {
// Let them all work in parallel. Since this will generate a ton of gdb traffic and traffic from other sources
// like variable, watch windows, things can fail. But our own backend queues things up so failures are unlikely
// With some other backend (if for instance we support cppdbg), not sure what happens. Worst case, try one OS
// at a time.
const promises = [];
for (const rtos of this.allRTOSes) {
promises.push(rtos.tryDetect(frameId));
}
Promise.all(promises).then((results) => {
for (const rtos of results) {
if (rtos.status === 'failed') {
const ix = this.allRTOSes.findIndex((v) => v === rtos);
this.allRTOSes.splice(ix, 1);
if (this.allRTOSes.length === 0) {
doRefresh();
break;
}
} else if (rtos.status === 'initialized') {
this.allRTOSes = [];
this.rtos = rtos;
doRefresh();
break;
}
}
if (this.allRTOSes.length > 0) {
// Some RTOSes have not finished detection
this.htmlContent.html = '<p>RTOS Views: RTOS detection in progress...</p>\n';
this.htmlContent.css = '';
resolve();
}
});
} else {
doRefresh();
}
});
}
public onContinued(): void {
this.lastFrameId = undefined;
if (this.rtos) {
this.rtos.onContinued();
}
}
public onExited(): void {
if (this.rtos) {
this.rtos.onExited();
}
this.lastFrameId = undefined;
this.rtos = undefined;
}
public refresh(): Promise<void> {
if (this.lastFrameId !== undefined) {
return this.onStopped(this.lastFrameId);
}
return new Promise<void>((r) => r());
}
}
interface DebugEventHandler {
onStarted(session: vscode.DebugSession): void;
onTerminated(session: vscode.DebugSession): void;
onStopped(session: vscode.DebugSession, frameId: number | undefined): void;
onContinued(session: vscode.DebugSession): void;
}
class MyDebugTracker {
constructor(public context: vscode.ExtensionContext, protected handler: DebugEventHandler) {
context.subscriptions
.push
// vscode.workspace.onDidChangeConfiguration(this.settingsChanged.bind(this))
();
// this.updateTrackedDebuggersFromSettings();
this.subscribeToTracker();
}
public isActive() {
return !!trackerApiClientInfo;
}
private subscribeToTracker(): Promise<boolean> {
return new Promise<boolean>((resolve) => {
DebugTracker.getTrackerExtension('rtos-views').then((ret) => {
if (ret instanceof Error) {
vscode.window.showErrorMessage(ret.message);
resolve(false);
} else {
trackerApi = ret;
const arg: IDebuggerTrackerSubscribeArg = {
version: 1,
body: {
debuggers: TrackedDebuggers,
handler: this.debugTrackerEventHandler.bind(this),
wantCurrentStatus: true,
notifyAllEvents: false,
// Make sure you set debugLevel to zero for production
debugLevel: 0,
},
};
const result = trackerApi.subscribe(arg);
if (typeof result === 'string') {
vscode.window.showErrorMessage(
`Subscription failed with extension 'debug-tracker-vscode' : ${result}`
);
resolve(false);
} else {
trackerApiClientInfo = result;
resolve(true);
}
}
});
});
}
static allSessions: { [sessionId: string]: vscode.DebugSession } = {};
async debugTrackerEventHandler(event: IDebuggerTrackerEvent) {
let session = event.session;
if (DebugSessionStatus.Initializing !== event.event) {
session = MyDebugTracker.allSessions[event.sessionId];
if (!session) {
// THis should not happen
console.error('rtos-views: Could not find session ' + event.sessionId);
return;
}
} else if (!session) {
console.error('Initializing but no session info?');
return;
}
switch (event.event) {
case DebugSessionStatus.Initializing: {
// Note that we can get initialized but never actually start the session due to errors
// so, we wait until we actually get a Started event
MyDebugTracker.allSessions[session.id] = session;
break;
}
case DebugSessionStatus.Started: {
this.handler.onStarted(session);
break;
}
case OtherDebugEvents.FirstStackTrace: {
// TODOL Technically, we don't need the frameId any more but it won't hurt to wait a bit
// until most of VSCode updates itself before we start queries
const frameId =
(event.stackTrace &&
event.stackTrace.body.stackFrames &&
event.stackTrace.body.stackFrames[0].id) ||
undefined;
this.handler.onStopped(session, frameId);
break;
}
case DebugSessionStatus.Running: {
this.handler.onContinued(session);
break;
}
case OtherDebugEvents.Capabilities: {
// Maybe we do something here
break;
}
case DebugSessionStatus.Terminated: {
delete MyDebugTracker.allSessions[event.sessionId];
this.handler.onTerminated(session);
break;
}
}
}
}
export class RTOSTracker implements DebugEventHandler {
private sessionMap: Map<string, RTOSSession> = new Map<string, RTOSSession>();
private provider: RTOSViewProvider;
private theTracker: MyDebugTracker;
public enabled: boolean;
public visible: boolean = false;
constructor(private context: vscode.ExtensionContext) {
this.provider = new RTOSViewProvider(context.extensionUri, this);
this.theTracker = new MyDebugTracker(context, this);
const config = vscode.workspace.getConfiguration('mcu-debug.rtos-views', null);
this.enabled = config.get('showRTOS', true);
RTOSCommon.RTOSBase.disableStackPeaks = config.get('disableStackPeaks', true);
vscode.commands.executeCommand('setContext', 'mcu-debug.rtos-views:showRTOS', this.enabled);
context.subscriptions.push(
vscode.window.registerWebviewViewProvider(RTOSViewProvider.viewType, this.provider),
vscode.workspace.onDidChangeConfiguration(this.settingsChanged.bind(this)),
vscode.commands.registerCommand('mcu-debug.rtos-views.toggleRTOSPanel', this.toggleRTOSPanel.bind(this)),
vscode.commands.registerCommand('mcu-debug.rtos-views.refresh', this.update.bind(this))
);
}
private settingsChanged(e: vscode.ConfigurationChangeEvent) {
if (e.affectsConfiguration('mcu-debug.rtos-views.showRTOS')) {
const config = vscode.workspace.getConfiguration('mcu-debug.rtos-views', null);
this.enabled = config.get('showRTOS', true);
vscode.commands.executeCommand('setContext', 'mcu-debug.rtos-views:showRTOS', this.enabled);
if (this.enabled) {
this.provider.showAndFocus();
}
this.update();
}
if (e.affectsConfiguration('mcu-debug.rtos-views.disableStackPeaks')) {
const config = vscode.workspace.getConfiguration('mcu-debug.rtos-views', null);
RTOSCommon.RTOSBase.disableStackPeaks = config.get('disableStackPeaks', false);
}
}
public onStopped(session: vscode.DebugSession, frameId: number | undefined) {
if (!frameId) {
return;
}
for (const rtosSession of this.sessionMap.values()) {
if (rtosSession.session.id === session.id) {
rtosSession.lastFrameId = frameId;
if (this.enabled && this.visible) {
rtosSession.onStopped(frameId).then(() => {
this.provider.updateHtml();
});
}
}
}
}
public onContinued(session: vscode.DebugSession) {
for (const rtosSession of this.sessionMap.values()) {
if (rtosSession.session.id === session.id) {
rtosSession.onContinued();
}
}
}
public onStarted(session: vscode.DebugSession) {
this.sessionMap.set(session.id, new RTOSSession(session));
}
public onTerminated(session: vscode.DebugSession) {
const s = this.sessionMap.get(session.id);
if (s) {
s.onExited();
this.sessionMap.delete(session.id);
}
}
// Only updates the RTOS state. Only debug sessions that are currently stopped will be updated
public async updateRTOSInfo(): Promise<any> {
const promises = [];
if (this.enabled && this.visible) {
for (const rtosSession of this.sessionMap.values()) {
promises.push(rtosSession.refresh());
}
}
return Promise.all(promises);
}
public toggleRTOSPanel() {
this.enabled = !this.enabled;
this.updateRTOSPanelStatus(this.enabled);
}
private updateRTOSPanelStatus(v: boolean) {
this.enabled = v;
const config = vscode.workspace.getConfiguration('mcu-debug.rtos-views', null);
config.update('showRTOS', this.enabled);
vscode.commands.executeCommand('setContext', 'mcu-debug.rtos-views:showRTOS', this.enabled);
if (this.enabled) {
this.provider.showAndFocus();
}
this.update();
}
public notifyPanelDisposed() {
this.visible = false;
}
public async visibilityChanged(v: boolean) {
if (v !== this.visible) {
this.visible = v;
if (this.visible) {
const msg = 'RTOS Views: Some sessions are busy. RTOS panel will be updated when session is paused';
for (const rtosSession of this.sessionMap.values()) {
if (rtosSession.lastFrameId === undefined) {
if (msg) {
vscode.window.showInformationMessage(msg);
break;
}
}
}
}
try {
await this.update();
} catch { }
}
}
// Updates RTOS state and the Panel HTML
private busyHtml: RTOSCommon.HtmlInfo | undefined;
public update(): Promise<void> {
return new Promise<void>((resolve) => {
if (!this.enabled || !this.visible || !this.sessionMap.size) {
resolve();
}
this.busyHtml = { html: /*html*/ '<h4>RTOS Views: Busy updating...</h4>\n', css: '' };
this.provider.updateHtml();
this.updateRTOSInfo().then(
() => {
this.busyHtml = undefined;
this.provider.updateHtml();
resolve();
},
(_e) => {
this.busyHtml = undefined;
this.provider.updateHtml();
resolve();
}
);
});
}
private lastGoodHtmlContent: RTOSCommon.HtmlInfo | undefined;
public getHtml(): RTOSCommon.HtmlInfo {
const ret: RTOSCommon.HtmlInfo = { html: '', css: '' };
if (this.busyHtml) {
return this.busyHtml;
} else if (this.sessionMap.size === 0) {
if (this.lastGoodHtmlContent) {
return this.lastGoodHtmlContent;
} else {
ret.html = '<p>RTOS Views: No active/compatible debug sessions running.</p>\n';
return ret;
}
} else if (!this.visible || !this.enabled) {
ret.html = '<p>RTOS Views: Contents are not visible, so no html generated</p>\n';
return ret;
}
for (const rtosSession of this.sessionMap.values()) {
const name = `RTOS Views: Session Name: "${rtosSession.session.name}"`;
if (!rtosSession.rtos) {
const nameAndStatus = name + ' -- No RTOS detected';
ret.html += /*html*/ `<h4>${nameAndStatus}</h4>\n`;
if (rtosSession.triedAndFailed) {
const supported = Object.keys(RTOS_TYPES).join(', ');
ret.html +=
`<p>RTOS Views: Failed to match any supported RTOS. Supported RTOSes are (${supported}). ` +
'Please report issues and/or contribute code/knowledge to add your RTOS</p>\n';
} else {
ret.html +=
/*html*/ '<p>RTOS Views: Try refreshing this panel. RTOS detection may be still in progress</p>\n';
}
} else {
const nameAndStatus =
name +
', ' +
rtosSession.rtos.name +
' detected.' +
(!rtosSession.htmlContent ? ' (No data available yet)' : '');
ret.html += /*html*/ `<h4>${nameAndStatus}</h4>\n` + rtosSession.htmlContent.html;
ret.css = rtosSession.htmlContent.css;
}
}
this.lastGoodHtmlContent = ret;
return ret;
}
}
class RTOSViewProvider implements vscode.WebviewViewProvider {
public static readonly viewType = 'rtos-views.rtos';
private webviewView: vscode.WebviewView | undefined;
constructor(private readonly extensionUri: vscode.Uri, private parent: RTOSTracker) { }
public resolveWebviewView(
webviewView: vscode.WebviewView,
_context: vscode.WebviewViewResolveContext,
_token: vscode.CancellationToken
) {
this.webviewView = webviewView;
this.parent.visible = this.webviewView.visible;
webviewView.webview.options = {
// Allow scripts in the webview
enableScripts: true,
localResourceRoots: [this.extensionUri]
};
this.webviewView.description = 'View RTOS internals';
this.webviewView.onDidDispose((_e) => {
this.webviewView = undefined;
this.parent.notifyPanelDisposed();
});
this.webviewView.onDidChangeVisibility((_e) => {
if (this.webviewView) {
this.parent.visibilityChanged(this.webviewView.visible);
}
});
this.updateHtml();
webviewView.webview.onDidReceiveMessage((msg) => {
switch (msg?.type) {
case 'refresh': {
this.parent.update();
break;
}
}
});
}
public showAndFocus() {
// The following does not require the webview to exist. It will be created if needed
vscode.commands.executeCommand('rtos-views.rtos.focus');
//
// Following will toggle our panel. Why it is named like that, I don't know. It makes no mention of XRTOS
// vscode.commands.executeCommand('workbench.view.extension.rtos-views');
}
public updateHtml() {
if (this.webviewView) {
this.webviewView.webview.html = this.getHtmlForWebview();
// console.log(this.webviewView.webview.html);
}
}
private getHtmlForWebview(): string {
const webview = this.webviewView?.webview;
if (!webview) {
return '';
}
if (!this.parent.enabled) {
return /*html*/ `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>RTOS Threads</title>
</head>
<body>
<p>Currently disabled. Enable setting "mcu-debug.rtos-views.showRTOS" or use Command "RTOS Views: Toggle RTOS Panel" to see any RTOS info</p>
</body>
</html>`;
}
const toolkitUri = getUri(webview, this.extensionUri, [
'node_modules',
'@vscode',
'webview-ui-toolkit',
'dist',
'toolkit.js',
]);
// Get the local path to main script run in the webview, then convert it to a uri we can use in the webview.
const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, 'dist', 'rtos-view.js'));
const rtosStyle = webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, 'resources', 'rtos.css'));
const htmlInfo = this.parent.getHtml();
// Use a nonce to only allow a specific script to be run.
const nonce = getNonce();
const ret = /*html*/ `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<!--
Use a content security policy to only allow loading images from https or from our extension directory,
and only allow scripts that have a specific nonce.
-->
<meta http-equiv="Content-Security-Policy" content="default-src 'none';
style-src 'nonce-${nonce}' ${webview.cspSource}; script-src 'nonce-${nonce}';">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="${rtosStyle}" rel="stylesheet">
<style nonce="${nonce}">
${htmlInfo.css}
</style>
<title>RTOS Threads</title>
</head>
<body>
${htmlInfo.html}
<script type="module" nonce="${nonce}" src="${toolkitUri}"></script>
<script type="module" nonce="${nonce}" src="${scriptUri}"></script>
</body>
</html>`;
writeHtmlToTmpDir(ret);
return ret;
}
}
function writeHtmlToTmpDir(str: string) {
try {
// eslint-disable-next-line no-constant-condition
if (false) {
const fname = path.join(os.tmpdir(), 'rtos.html');
console.log(`Write HTML to file ${fname}`);
fs.writeFileSync(fname, str);
}
} catch (e) {
console.log(e ? e.toString() : 'unknown exception?');
}
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function appendMsgToTmpDir(str: string) {
try {
// eslint-disable-next-line no-constant-condition
if (false) {
const fname = path.join(os.tmpdir(), 'rtos-msgs.txt');
console.log(`Write ${str} to file ${fname}`);
if (!str.endsWith('\n')) {
str = str + '\n';
}
fs.appendFileSync(fname, str);
}
} catch (e) {
console.log(e ? e.toString() : 'unknown exception?');
}
}
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;
}
export function getUri(webview: vscode.Webview, extensionUri: vscode.Uri, pathList: string[]) {
return webview.asWebviewUri(vscode.Uri.joinPath(extensionUri, ...pathList));
}