Skip to content

Commit

Permalink
Support for setting values on expression from the Watch panel
Browse files Browse the repository at this point in the history
  • Loading branch information
haneefdm committed Jan 18, 2025
1 parent 3e6bb5f commit d727d68
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 68 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# ChangeLog

# V1.13.0-pre3
# V1.13.0-pre4
* Feature: Support for setting values on expression from the Watch panel. The new value has to be something that GDB understands, so setting values on arrays, structures won't work but it should work on any scalar
* MAJOR Change. The `Restart` button functionality has been completely changed. This was not a stable function and VSCode kept changing its definition over the years multiple times. However they provide a default functionality, so the `Restart` button still works but very differently from our implementation. As of today, VSCode seems to do the following (and this extension is not involved)
* It Stops the current session. This means the current GDB and any GDB-server (openocd, stlink, etc. are also terminated)
* If then Starts a new session using the same configuration.
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "1.13.0-pre3",
"version": "1.13.0-pre4",
"preview": false,
"activationEvents": [
"onDebugResolve:cortex-debug",
Expand Down
29 changes: 21 additions & 8 deletions src/backend/mi2/mi2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -892,22 +892,27 @@ export class MI2 extends EventEmitter implements IBackend {
x: 'hexadecimal'
};

public async varCreate(
parent: number, expression: string, name: string = '-', scope: string = '@',
threadId?: number, frameId?: number): Promise<VariableObject> {
if (trace) {
this.log('stderr', 'varCreate');
}
let fmt = null;
private getExprAndFmt(expression: string): [string, string] {
let fmt = '';
expression = expression.trim();
if (/,[bdhonx]$/i.test(expression)) {
fmt = expression.substring(expression.length - 1).toLocaleLowerCase();
expression = expression.substring(0, expression.length - 2);
}
expression = expression.replace(/"/g, '\\"');
return [expression, fmt];
}

public async varCreate(
parent: number, expression: string, name: string = '-', scope: string = '@',
threadId?: number, frameId?: number): Promise<VariableObject> {
if (trace) {
this.log('stderr', 'varCreate');
}

const [expr, fmt] = this.getExprAndFmt(expression);
const thFr = ((threadId !== undefined) && (frameId !== undefined)) ? `--thread ${threadId} --frame ${frameId}` : '';
const createResp = await this.sendCommand(`var-create ${thFr} ${name} ${scope} "${expression}"`);
const createResp = await this.sendCommand(`var-create ${thFr} ${name} ${scope} "${expr}"`);
let overrideVal: string = null;
if (fmt && name !== '-') {
const formatResp = await this.sendCommand(`var-set-format ${name} ${MI2.FORMAT_SPEC_MAP[fmt]}`);
Expand Down Expand Up @@ -972,6 +977,14 @@ export class MI2 extends EventEmitter implements IBackend {
return this.sendCommand(`var-assign ${MI2.getThreadFrameStr(threadId, frameId)} ${name} ${rawValue}`);
}

public async exprAssign(expr: string, rawValue: string, threadId: number, frameId: number): Promise<MINode> {
if (trace) {
this.log('stderr', 'exprAssign');
}
const [lhs, fmt] = this.getExprAndFmt(expr);
return this.sendCommand(`var-assign ${MI2.getThreadFrameStr(threadId, frameId)} ${lhs} ${rawValue}`);
}

public logNoNewLine(type: string, msg: string) {
this.emit('msg', type, msg);
}
Expand Down
154 changes: 98 additions & 56 deletions src/gdb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,7 @@ export class GDBDebugSession extends LoggingDebugSession {
response.body.supportsFunctionBreakpoints = true;
response.body.supportsEvaluateForHovers = true;
response.body.supportsSetVariable = true;
response.body.supportsSetExpression = true;

// We no longer support a 'Restart' request. However, VSCode will implement a replacement by terminating the
// current session and starting a new one from scratch. But, we still have to support the launch.json
Expand Down Expand Up @@ -1938,6 +1939,8 @@ export class GDBDebugSession extends LoggingDebugSession {
}
}

// Note that setVariableRequest is called to set member variables of watched expressions. So, don't
// make assumptions of what names you might see
protected async setVariableRequest(response: DebugProtocol.SetVariableResponse, args: DebugProtocol.SetVariableArguments): Promise<void> {
try {
let name = args.name;
Expand Down Expand Up @@ -1985,7 +1988,36 @@ export class GDBDebugSession extends LoggingDebugSession {
};
this.sendResponse(response);
} catch (err) {
this.sendErrorResponse(response, 11, `Could not set variable: ${err}`);
this.sendErrorResponse(response, 11, `Could not set variable '${args.name}'\n${err}`);
}
}

protected async setExpressionRequest(response: DebugProtocol.SetVariableResponse, args: DebugProtocol.SetExpressionArguments): Promise<void> {
try {
const [threadId, frameId] = args.frameId ? decodeReference(args.frameId) : [undefined, undefined];
const exp = args.expression;
const varObjName = this.createVarNameFromExpr(args.expression, args.frameId, 'watch');
let varId = this.variableHandlesReverse.get(varObjName);
if (varId === undefined) {
let varObj: VariableObject;
if (args.frameId === undefined) {
varObj = await this.miDebugger.varCreate(0, exp, varObjName, '@'); // Create floating variable
} else {
varObj = await this.miDebugger.varCreate(0, exp, varObjName, '@', threadId, frameId);
}
varId = this.findOrCreateVariable(varObj);
varObj.exp = exp;
varObj.id = varId;
}
await this.miDebugger.exprAssign(varObjName, args.value, threadId, frameId);
const evalRsp = await this.evalExprInternal(args.expression, args.frameId, 'watch', threadId, frameId);
response.body = {
...evalRsp,
value: evalRsp.result,
};
this.sendResponse(response);
} catch (err) {
this.sendErrorResponse(response, 11, `Could not set expression '${args.expression}'\n${err}`);
}
}

Expand Down Expand Up @@ -3204,61 +3236,7 @@ export class GDBDebugSession extends LoggingDebugSession {

if (args.context !== 'repl') {
try {
const exp = args.expression;
const hasher = crypto.createHash('sha256');
hasher.update(exp);
if (args.frameId !== undefined) {
hasher.update(args.frameId.toString(16));
}
const exprName = hasher.digest('hex');
const varObjName = `${args.context}_${exprName}`;
let varObj: VariableObject;
let varId = this.variableHandlesReverse.get(varObjName);
let createNewVar = varId === undefined;
let updateError;
if (!createNewVar) {
try {
const changes = await this.miDebugger.varUpdate(varObjName, threadId, frameId);
const changelist = changes.result('changelist');
for (const change of changelist || []) {
const inScope = MINode.valueOf(change, 'in_scope');
if (inScope === 'true') {
const name = MINode.valueOf(change, 'name');
const vId = this.variableHandlesReverse.get(name);
const v = this.variableHandles.get(vId) as any;
v.applyChanges(change);
} else {
const msg = `${exp} currently not in scope`;
await this.miDebugger.sendCommand(`var-delete ${varObjName}`);
if (this.args.showDevDebugOutput) {
this.handleMsg('log', `Expression ${msg}. Will try to create again\n`);
}
createNewVar = true;
throw new Error(msg);
}
}
varObj = this.variableHandles.get(varId) as any;
} catch (err) {
updateError = err;
}
}
if (!this.isBusy() && (createNewVar || ((updateError instanceof MIError && updateError.message === VarNotFoundMsg)))) {
// We always create a floating variable so it will be updated in the context of the current frame
// Technicall, we should be able to bind this to this frame but for some reason gdb gets confused
// from previous stack frames and returns the wrong results or says nothing changed when in fact it has
if (args.frameId === undefined) {
varObj = await this.miDebugger.varCreate(0, exp, varObjName, '@'); // Create floating variable
} else {
varObj = await this.miDebugger.varCreate(0, exp, varObjName, '@', threadId, frameId);
}

varId = this.findOrCreateVariable(varObj);
varObj.exp = exp;
varObj.id = varId;
} else if (!varObj) {
throw updateError || new Error('evaluateRequest: unknown error');
}
response.body = varObj.toProtocolEvaluateResponseBody();
response.body = await this.evalExprInternal(args.expression, args.frameId, args.context, threadId, frameId);
this.sendResponse(response);
} catch (err) {
if (this.isBusy()) {
Expand Down Expand Up @@ -3315,6 +3293,70 @@ export class GDBDebugSession extends LoggingDebugSession {
return this.evaluateQ.add(doit, r, a);
}

private async evalExprInternal(
exp: string, frameRef: number | undefined, context: string,
threadId: number, frameId: number): Promise<DebugProtocol.EvaluateResponse['body']> {
const varObjName = this.createVarNameFromExpr(exp, frameRef, context);
let varObj: VariableObject;
let varId = this.variableHandlesReverse.get(varObjName);
let createNewVar = varId === undefined;
let updateError;
if (!createNewVar) {
try {
const changes = await this.miDebugger.varUpdate(varObjName, threadId, frameId);
const changelist = changes.result('changelist');
for (const change of changelist || []) {
const inScope = MINode.valueOf(change, 'in_scope');
if (inScope === 'true') {
const name = MINode.valueOf(change, 'name');
const vId = this.variableHandlesReverse.get(name);
const v = this.variableHandles.get(vId) as any;
v.applyChanges(change);
} else {
const msg = `${exp} currently not in scope`;
await this.miDebugger.sendCommand(`var-delete ${varObjName}`);
if (this.args.showDevDebugOutput) {
this.handleMsg('log', `Expression ${msg}. Will try to create again\n`);
}
createNewVar = true;
throw new Error(msg);
}
}
varObj = this.variableHandles.get(varId) as any;
} catch (err) {
updateError = err;
}
}
if (!this.isBusy() && (createNewVar || ((updateError instanceof MIError && updateError.message === VarNotFoundMsg)))) {
// We always create a floating variable so it will be updated in the context of the current frame
// Technicall, we should be able to bind this to this frame but for some reason gdb gets confused
// from previous stack frames and returns the wrong results or says nothing changed when in fact it has
if (frameRef === undefined) {
varObj = await this.miDebugger.varCreate(0, exp, varObjName, '@'); // Create floating variable
} else {
varObj = await this.miDebugger.varCreate(0, exp, varObjName, '@', threadId, frameId);
}

varId = this.findOrCreateVariable(varObj);
varObj.exp = exp;
varObj.id = varId;
} else if (!varObj) {
throw updateError || new Error('evaluateRequest: unknown error');
}
return varObj.toProtocolEvaluateResponseBody();
}

private createVarNameFromExpr(exp: string, encodedFrameId: number | undefined, context: string): string {
const hasher = crypto.createHash('sha256');
hasher.update(exp);
if (encodedFrameId !== undefined) {
hasher.update(encodedFrameId.toString(16));
}
const exprName = hasher.digest('hex');
const varObjName = `${context || 'watch'}_${exprName}`;
return varObjName;
}

protected async gotoTargetsRequest(response: DebugProtocol.GotoTargetsResponse, args: DebugProtocol.GotoTargetsArguments): Promise<void> {
try {
const done = await this.miDebugger.goto(args.source.path, args.line);
Expand Down

0 comments on commit d727d68

Please sign in to comment.