Skip to content

feat: Exception Stack Trace Decoder for esp32 #1893

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ import {
ConfigServicePath,
} from '../common/protocol/config-service';
import { MonitorWidget } from './serial/monitor/monitor-widget';
import { DecodeWidget } from './serial/decode/decode-widget';
import { MonitorViewContribution } from './serial/monitor/monitor-view-contribution';
import { TabBarDecoratorService as TheiaTabBarDecoratorService } from '@theia/core/lib/browser/shell/tab-bar-decorator';
import { TabBarDecoratorService } from './theia/core/tab-bar-decorator';
Expand Down Expand Up @@ -321,6 +322,7 @@ import { InterfaceScale } from './contributions/interface-scale';
import { OpenHandler } from '@theia/core/lib/browser/opener-service';
import { NewCloudSketch } from './contributions/new-cloud-sketch';
import { SketchbookCompositeWidget } from './widgets/sketchbook/sketchbook-composite-widget';
import { DecodeViewContribution } from './serial/decode/decode-view';
import { WindowTitleUpdater } from './theia/core/window-title-updater';
import { WindowTitleUpdater as TheiaWindowTitleUpdater } from '@theia/core/lib/browser/window/window-title-updater';
import { ThemeServiceWithDB } from './theia/core/theming';
Expand Down Expand Up @@ -480,6 +482,21 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
.inSingletonScope();
bind(CoreErrorHandler).toSelf().inSingletonScope();

// Decode box
bind(DecodeWidget).toSelf();
bindViewContribution(bind, DecodeViewContribution);
bind(TabBarToolbarContribution).toService(DecodeViewContribution);
bind(WidgetFactory).toDynamicValue((context) => ({
id: DecodeWidget.ID,
createWidget: () => {
return new DecodeWidget(
context.container.get<ConfigService>(ConfigService),
context.container.get<BoardsServiceProvider>(BoardsServiceProvider),
context.container.get<SketchesServiceClientImpl>(SketchesServiceClientImpl)
);
},
}));

// Serial monitor
bind(MonitorWidget).toSelf();
bind(FrontendApplicationContribution).toService(MonitorModel);
Expand Down
150 changes: 150 additions & 0 deletions arduino-ide-extension/src/browser/serial/decode/decode-output.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import * as React from '@theia/core/shared/react';
import { Event } from '@theia/core/lib/common/event';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { spawnCommand } from '../../../node/exec-util';

export type Line = { message: string; lineLen: number };
export type Element = {
address: string;
function: string;
path: {
value: string;
isLink: boolean;
}
lineNumber: string
};

export class DecodeOutput extends React.Component<
DecodeOutput.Props,
DecodeOutput.State
> {
/**
* Do not touch it. It is used to be able to "follow" the serial monitor log.
*/
protected toDisposeBeforeUnmount = new DisposableCollection();

constructor(props: Readonly<DecodeOutput.Props>) {
super(props);
this.state = {
elements: [],
};
}

// If a string of <digit>.<digit>.<digit> or <digit>.<digit> is found, replaces it with "*"
changeVersionToAny = (path: string) => {
const regex = new RegExp(/(\d\.\d\.\d)|(\d\.\d)/g);
const found = path.match(regex);
if(found) {
return path.replace(found[0], "*")
}
return path
}

isClientPath = async (path:string): Promise<boolean> => {
return await spawnCommand("cd", [
path
], (err) => err)
.then((data) => true)
.catch(err => false)
}

openFinder = async (path:string) => {
await spawnCommand("open", [
path
]);
}

retrievePath = (dirPath:string) => {
return dirPath.substring(0,dirPath.lastIndexOf("/")+1);
}

decodeText = async (value: string) => {
const lines = value.split("\n");

// Remove the extra newline at the end
lines.pop();
const elements : Array<Element> = [];
for(let i=0;i<lines.length;i++) {
let line = lines[i].split(/(?!\(.*)\s(?![^(]*?\))/g);
if(line[0] === "") {
line.shift();
}
let pathSplit = line[3].split(":");
pathSplit[0] = this.changeVersionToAny(pathSplit[0]);
let obj: Element = {
address: line[0],
function: line[1],
path: {
value: pathSplit[0],
isLink: false,
},
lineNumber: pathSplit[1]
};
if(await this.isClientPath(this.retrievePath(pathSplit[0]))) {
obj = {
address: line[0],
function: line[1],
path: {
value: pathSplit[0],
isLink: true,
},
lineNumber: pathSplit[1]
};
}
elements.push(obj);
}
this.setState({ elements });
};

override render(): React.ReactNode {
return (
<div style={{ whiteSpace: 'pre-wrap' }}>
{this.state.elements.map((element) => (
<div style={{display: "inline-block"}}>
<span style={{color: "green"}}>{element.address} </span>
<span style={{color: "blue", fontWeight: "bold"}}>{element.function} </span>
at
{ element.path.isLink ? <a><span onClick={async () => await this.openFinder(this.retrievePath(element.path.value))}>{element.path.value}</span></a> : <span> {element.path.value} </span> }
line
<span style={{fontWeight: "bold"}}> {element.lineNumber}</span>
</div>
))}
</div>
);
}

override shouldComponentUpdate(): boolean {
return true;
}

override componentDidMount(): void {
this.toDisposeBeforeUnmount.pushAll([
this.props.clearConsoleEvent(() =>
this.setState({ elements: [] })
),
]);
}

override componentWillUnmount(): void {
// TODO: "Your preferred browser's local storage is almost full." Discard `content` before saving layout?
this.toDisposeBeforeUnmount.dispose();
}
}

export namespace DecodeOutput {
export interface Props {
readonly clearConsoleEvent: Event<void>;
readonly height: number;
}

export interface State {
elements: Element[];
}

export interface SelectOption<T> {
readonly label: string;
readonly value: T;
}

export const MAX_CHARACTERS = 1_000_000;
}
135 changes: 135 additions & 0 deletions arduino-ide-extension/src/browser/serial/decode/decode-send-input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import * as React from '@theia/core/shared/react';
import { Key, KeyCode } from '@theia/core/lib/browser/keys';
import { DisposableCollection } from '@theia/core/lib/common';

class HistoryList {
private readonly items: string[] = [];
private index = -1;

constructor(private readonly size = 100) {}

push(val: string): void {
if (val !== this.items[this.items.length - 1]) {
this.items.push(val);
}
while (this.items.length > this.size) {
this.items.shift();
}
this.index = -1;
}

previous(): string {
if (this.index === -1) {
this.index = this.items.length - 1;
return this.items[this.index];
}
if (this.hasPrevious) {
return this.items[--this.index];
}
return this.items[this.index];
}

private get hasPrevious(): boolean {
return this.index >= 1;
}

next(): string {
if (this.index === this.items.length - 1) {
this.index = -1;
return '';
}
if (this.hasNext) {
return this.items[++this.index];
}
return '';
}

private get hasNext(): boolean {
return this.index >= 0 && this.index !== this.items.length - 1;
}
}

export namespace DecodeSendInput {
export interface Props {
readonly onSend: (text: string) => void;
readonly resolveFocus: (element: HTMLElement | undefined) => void;
}
export interface State {
text: string;
history: HistoryList;
}
}

export class DecodeSendInput extends React.Component<
DecodeSendInput.Props,
DecodeSendInput.State
> {
protected toDisposeBeforeUnmount = new DisposableCollection();

constructor(props: Readonly<DecodeSendInput.Props>) {
super(props);
this.state = { text: '', history: new HistoryList() };
this.onChange = this.onChange.bind(this);
this.onSend = this.onSend.bind(this);
this.onKeyDown = this.onKeyDown.bind(this);
}

override componentWillUnmount(): void {
// TODO: "Your preferred browser's local storage is almost full." Discard `content` before saving layout?
this.toDisposeBeforeUnmount.dispose();
}

override render(): React.ReactNode {
return (
<input
ref={this.setRef}
type="text"
className='theia-input'
placeholder={this.placeholder}
value={this.state.text}
onChange={this.onChange}
onKeyDown={this.onKeyDown}
/>
);
}

protected get placeholder(): string {

return 'Enter backtrace text to decode. (Ex: Backtrace: 0x40086e7c:0x3ffb4ff0...)';
}

protected setRef = (element: HTMLElement | null): void => {
if (this.props.resolveFocus) {
this.props.resolveFocus(element || undefined);
}
};

protected onChange(event: React.ChangeEvent<HTMLInputElement>): void {
this.setState({ text: event.target.value });
}

protected onSend(): void {
this.props.onSend(this.state.text);
this.setState({ text: '' });
}

protected onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void {
const keyCode = KeyCode.createKeyCode(event.nativeEvent);
if (keyCode) {
const { key } = keyCode;
if (key === Key.ENTER) {
const { text } = this.state;
this.onSend();
if (text) {
this.state.history.push(text);
}
} else if (key === Key.ARROW_UP) {
this.setState({ text: this.state.history.previous() });
} else if (key === Key.ARROW_DOWN) {
this.setState({ text: this.state.history.next() });
} else if (key === Key.ESCAPE) {
this.setState({ text: '' });
}
}
}
}
Loading