Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ export abstract class ApplicationOperations {
abstract selectDomElement(position: ElementPosition, target: Frame): void;
abstract inspect(directivePosition: DirectivePosition, objectPath: string[], target: Frame): void;
abstract inspectSignal(position: SignalNodePosition, target: Frame): void;
abstract setSignalBreakpoint(position: SignalNodePosition, target: Frame): void;
abstract removeSignalBreakpoint(position: SignalNodePosition, target: Frame): void;
abstract getActiveSignalBreakpoints(target: Frame): Promise<SignalNodePosition[]>;
abstract viewSourceFromRouter(name: string, type: string, target: Frame): void;
abstract setStorageItems(items: {[key: string]: unknown}): Promise<void>;
abstract getStorageItems(items: string[]): Promise<{[key: string]: unknown}>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,18 @@ export class AppOperationsMock extends ApplicationOperations {
throw new Error('Method not implemented.');
}

override setSignalBreakpoint(position: SignalNodePosition, target: Frame): void {
throw new Error('Method not implemented.');
}

override removeSignalBreakpoint(position: SignalNodePosition, target: Frame): void {
throw new Error('Method not implemented.');
}

override getActiveSignalBreakpoints(target: Frame): Promise<SignalNodePosition[]> {
return Promise.resolve([]);
}

override viewSourceFromRouter(name: string, type: string, target: Frame): void {
throw new Error('Method not implemented.');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@
[node]="node"
[graph]="signalGraph.graph()!"
[element]="signalGraph.element()!"
[hasBreakpoint]="hasBreakpoint(node)"
(gotoSource)="gotoSource($event)"
(setBreakpoint)="setBreakpoint($event)"
(removeBreakpoint)="removeBreakpoint($event)"
(expandCluster)="expandCluster($event)"
(highlightDeps)="highlightDeps($event)"
(close)="detailsVisible.set(false)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import {
Component,
computed,
effect,
inject,
input,
linkedSignal,
Expand Down Expand Up @@ -45,6 +46,25 @@ export class SignalGraphPaneComponent {
private readonly appOperations = inject(ApplicationOperations);
private readonly frameManager = inject(FrameManager);

constructor() {
effect(() => {
const element = this.signalGraph.element();
const graph = this.signalGraph.graph();
if (element && graph) {
const frame = this.frameManager.selectedFrame();
this.appOperations.getActiveSignalBreakpoints(frame!).then((positions) => {
const activeIds = new Set<string>();
for (const pos of positions) {
if (JSON.stringify(pos.element) === JSON.stringify(element)) {
activeIds.add(pos.signalId);
}
}
this.activeBreakpoints.set(activeIds);
});
}
});
}

protected readonly close = output<void>();

// Source for selected node ID.
Expand Down Expand Up @@ -86,6 +106,18 @@ export class SignalGraphPaneComponent {

protected readonly detailsVisible = signal(false);

// Track active breakpoints for the current inspected element.
// Resets when the inspected element changes.
protected readonly activeBreakpoints = linkedSignal<ElementPosition | undefined, Set<string>>({
source: () => this.signalGraph.element(),
computation: () => new Set(),
});

protected hasBreakpoint(node: DevtoolsSignalGraphNode | undefined): boolean {
if (!node) return false;
return this.activeBreakpoints().has(node.id);
}

protected empty = computed(() => !(this.signalGraph.graph()?.nodes.length! > 0));

onNodeClick(node: DevtoolsSignalGraphNode) {
Expand All @@ -104,6 +136,38 @@ export class SignalGraphPaneComponent {
);
}

setBreakpoint(node: DevtoolsSignalGraphNode) {
const frame = this.frameManager.selectedFrame();
this.appOperations.setSignalBreakpoint(
{
element: this.signalGraph.element()!,
signalId: node.id,
},
frame!,
);
this.activeBreakpoints.update((set) => {
const newSet = new Set(set);
newSet.add(node.id);
return newSet;
});
}

removeBreakpoint(node: DevtoolsSignalGraphNode) {
const frame = this.frameManager.selectedFrame();
this.appOperations.removeSignalBreakpoint(
{
element: this.signalGraph.element()!,
signalId: node.id,
},
frame!,
);
this.activeBreakpoints.update((set) => {
const newSet = new Set(set);
newSet.delete(node.id);
return newSet;
});
}

expandCluster(clusterId: string) {
this.visualizer().expandCluster(clusterId);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,17 @@
>
<mat-icon> code </mat-icon>
</button>
<button
ng-button
size="compact"
class="action-btn"
btnType="secondary"
(click)="hasBreakpoint() ? removeBreakpoint.emit(node) : setBreakpoint.emit(node)"
[disabled]="!node.debuggable"
[matTooltip]="hasBreakpoint() ? 'Remove breakpoint' : 'Set breakpoint'"
>
<mat-icon [style.color]="hasBreakpoint() ? '#1a73e8' : 'inherit'"> label </mat-icon>
</button>
} @else if (isClusterNode) {
<button
ng-button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ export class SignalDetailsComponent {
protected readonly element = input.required<ElementPosition>();

protected readonly gotoSource = output<DevtoolsSignalGraphNode>();
protected readonly setBreakpoint = output<DevtoolsSignalGraphNode>();
protected readonly removeBreakpoint = output<DevtoolsSignalGraphNode>();
protected readonly hasBreakpoint = input<boolean>(false);
protected readonly expandCluster = output<string>();
protected readonly highlightDeps = output<{
node: DevtoolsSignalGraphNode;
Expand Down
179 changes: 179 additions & 0 deletions devtools/projects/shell-browser/src/app/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,183 @@ if (chrome !== undefined && chrome.runtime !== undefined) {

const tabs = {};
TabManager.initialize(tabs);

const scriptMap = new Map<string, string>();

chrome.debugger.onEvent.addListener((source, method, params) => {
if (method === 'Debugger.scriptParsed' && params) {
const {scriptId, url} = params as any;
scriptMap.set(scriptId, url);
}
});

const activeBreakpoints = new Map<number, Map<string, string>>();

function serializePosition(position: any): string {
return JSON.stringify({
element: position.element,
signalId: position.signalId,
});
}

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === 'setSignalBreakpoint') {
const {tabId, position} = message;
setBreakpointViaCDP(tabId, position)
.then((result) => sendResponse({success: true, result}))
.catch((err) => {
console.error('CDP Error:', err);
sendResponse({success: false, error: err.message || err});
});
return true; // Keep channel open
} else if (message.action === 'removeSignalBreakpoint') {
const {tabId, position} = message;
removeBreakpointViaCDP(tabId, position)
.then((result) => sendResponse({success: true, result}))
.catch((err) => {
console.error('CDP Error:', err);
sendResponse({success: false, error: err.message || err});
});
return true; // Keep channel open
} else if (message.action === 'getActiveSignalBreakpoints') {
const {tabId} = message;
const tabBreakpoints = activeBreakpoints.get(tabId);
const activePositions: any[] = [];
if (tabBreakpoints) {
for (const posKey of tabBreakpoints.keys()) {
try {
activePositions.push(JSON.parse(posKey));
} catch {}
}
}
sendResponse({success: true, activePositions});
return true;
}
return false;
});

function attachDebugger(target: chrome.debugger.Debuggee, version: string): Promise<void> {
return new Promise((resolve, reject) => {
chrome.debugger.attach(target, version, () => {
if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError);
} else {
resolve();
}
});
});
}

function sendDebuggerCommand(
target: chrome.debugger.Debuggee,
method: string,
commandParams?: {[key: string]: any},
): Promise<any> {
return new Promise((resolve, reject) => {
chrome.debugger.sendCommand(target, method, commandParams, (result) => {
if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError);
} else {
resolve(result);
}
});
});
}

async function setBreakpointViaCDP(tabId: number, position: any) {
const target = {tabId};

try {
await attachDebugger(target, '1.3');
} catch (err: any) {
const msg = err.message ? String(err.message).toLowerCase() : '';
if (!msg.includes('already attached')) {
throw err;
}
}

await sendDebuggerCommand(target, 'Debugger.enable');

const expression = `inspectedApplication.findSignalNodeByPosition('${JSON.stringify(position)}')`;
const evalResult = await sendDebuggerCommand(target, 'Runtime.evaluate', {
expression,
objectGroup: 'angular-devtools',
});

if (evalResult.exceptionDetails) {
throw new Error('Evaluation failed: ' + evalResult.exceptionDetails.exception.description);
}

const objectId = evalResult.result.objectId;
if (!objectId) {
throw new Error('Could not find function object');
}

const propsResult = await sendDebuggerCommand(target, 'Runtime.getProperties', {
objectId,
});

const internalProps = propsResult.internalProperties || [];
const locationProp = internalProps.find((p: any) => p.name === '[[FunctionLocation]]');

if (!locationProp || !locationProp.value || !locationProp.value.value) {
throw new Error('Could not find [[FunctionLocation]]');
}

const {scriptId, lineNumber, columnNumber} = locationProp.value.value;

let bpResult;
let url = scriptMap.get(scriptId);
if (!url) {
await new Promise((resolve) => setTimeout(resolve, 150));
url = scriptMap.get(scriptId);
}

if (!url) {
console.warn('Could not find URL for scriptId:', scriptId, 'falling back to scriptId');
bpResult = await sendDebuggerCommand(target, 'Debugger.setBreakpoint', {
location: {scriptId, lineNumber, columnNumber},
});
} else {
bpResult = await sendDebuggerCommand(target, 'Debugger.setBreakpointByUrl', {
url,
lineNumber,
columnNumber,
});
}

const breakpointId = bpResult.breakpointId;

if (bpResult && bpResult.breakpointId) {
if (!activeBreakpoints.has(tabId)) {
activeBreakpoints.set(tabId, new Map());
}
const posKey = serializePosition(position);
activeBreakpoints.get(tabId)!.set(posKey, bpResult.breakpointId);
}

return bpResult;
}

async function removeBreakpointViaCDP(tabId: number, position: any) {
const target = {tabId};
const tabBps = activeBreakpoints.get(tabId);
if (!tabBps) {
throw new Error('No active breakpoints for this tab');
}
const posKey = serializePosition(position);
const breakpointId = tabBps.get(posKey);
if (!breakpointId) {
throw new Error('No active breakpoint found for this signal');
}

await sendDebuggerCommand(target, 'Debugger.removeBreakpoint', {
breakpointId,
});

tabBps.delete(posKey);
if (tabBps.size === 0) {
activeBreakpoints.delete(tabId);
}
}
}
Loading
Loading