Skip to content
This repository has been archived by the owner on Apr 3, 2024. It is now read-only.

feat!: Add ability to store breakpoint data in firebase realtime database #1076

Merged
merged 4 commits into from
May 12, 2022
Merged
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"console-log-level": "^1.4.0",
"extend": "^3.0.2",
"findit2": "^2.2.3",
"firebase-admin": "^9.11.1",
"gcp-metadata": "^4.0.0",
"p-limit": "^3.0.1",
"semver": "^7.0.0",
Expand Down
17 changes: 17 additions & 0 deletions src/agent/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,21 @@ export interface ResolvedDebugAgentConfig extends GoogleAuthOptions {
* in defaultConfig.
*/
resetV8DebuggerThreshold: number;

/**
* If set, use Firebase Realtime Database as the backend instead of the
* Cloud Debugger API. Requires many things, which will be documented later.
*/
useFirebase: boolean;

/**
* If set, use this key for Firebase activities instead of default google credentials.
*/
firebaseKeyPath?: string;
/**
* If set, use this as the firebase database url. If not set, a FIXME default will be used.
*/
firebaseDbUrl?: string;
}

export interface StackdriverConfig extends GoogleAuthOptions {
Expand Down Expand Up @@ -412,4 +427,6 @@ export const defaultConfig: ResolvedDebugAgentConfig = {
forceNewAgent_: false,
testMode_: false,
resetV8DebuggerThreshold: 30,

useFirebase: false,
};
163 changes: 17 additions & 146 deletions src/agent/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,46 +16,14 @@
* @module debug/controller
*/

import {ServiceObject} from '@google-cloud/common';
import * as assert from 'assert';
import * as qs from 'querystring';
import * as t from 'teeny-request';

import {URL} from 'url';

import {Debug} from '../client/stackdriver/debug';
import {Debuggee} from '../debuggee';
import * as stackdriver from '../types/stackdriver';

export class Controller extends ServiceObject {
private nextWaitToken: string | null;
private agentId: string | null;

apiUrl: string;

/**
* @constructor
*/

constructor(debug: Debug, config?: {apiUrl?: string}) {
super({parent: debug, baseUrl: '/controller'});

/** @private {string} */
this.nextWaitToken = null;
this.agentId = null;

this.apiUrl = `https://${debug.apiEndpoint}/v2/controller`;

if (config && config.apiUrl) {
this.apiUrl = config.apiUrl + new URL(this.apiUrl).pathname;
}
}

export interface Controller {
/**
* Register to the API (implementation)
*
* @param {!function(?Error,Object=)} callback
* @private
*/
register(
debuggee: Debuggee,
Expand All @@ -66,91 +34,7 @@ export class Controller extends ServiceObject {
agentId: string;
}
) => void
): void {
const options = {
uri: this.apiUrl + '/debuggees/register',
method: 'POST',
json: true,
body: {debuggee},
};
this.request(
options,
(err, body: {debuggee: Debuggee; agentId: string}, response) => {
if (err) {
callback(err);
} else if (response!.statusCode !== 200) {
callback(
new Error('unable to register, statusCode ' + response!.statusCode)
);
} else if (!body.debuggee) {
callback(new Error('invalid response body from server'));
} else {
debuggee.id = body.debuggee.id;
this.agentId = body.agentId;
callback(null, body);
}
}
);
}

/**
* Fetch the list of breakpoints from the server. Assumes we have registered.
* @param {!function(?Error,Object=,Object=)} callback accepting (err, response,
* body)
*/
listBreakpoints(
debuggee: Debuggee,
callback: (
err: Error | null,
response?: t.Response,
body?: stackdriver.ListBreakpointsResponse
) => void
): void {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const that = this;
assert(debuggee.id, 'should have a registered debuggee');
const query: stackdriver.ListBreakpointsQuery = {successOnTimeout: true};
if (that.nextWaitToken) {
query.waitToken = that.nextWaitToken;
}
if (that.agentId) {
query.agentId = that.agentId;
}

const uri =
this.apiUrl +
'/debuggees/' +
encodeURIComponent(debuggee.id) +
'/breakpoints?' +
qs.stringify(query as qs.ParsedUrlQueryInput);
that.request(
{uri, json: true},
(err, body: stackdriver.ListBreakpointsResponse, response) => {
if (!response) {
callback(
err || new Error('unknown error - request response missing')
);
return;
} else if (response.statusCode === 404) {
// The v2 API returns 404 (google.rpc.Code.NOT_FOUND) when the agent
// registration expires. We should re-register.
callback(null, response as {} as t.Response);
return;
} else if (response.statusCode !== 200) {
callback(
new Error(
'unable to list breakpoints, status code ' + response.statusCode
)
);
return;
} else {
body = body || {};
that.nextWaitToken = body.nextWaitToken;
callback(null, response as {} as t.Response, body);
}
}
);
}
): void;

/**
* Update the server about breakpoint state
Expand All @@ -162,34 +46,21 @@ export class Controller extends ServiceObject {
debuggee: Debuggee,
breakpoint: stackdriver.Breakpoint,
callback: (err?: Error, body?: {}) => void
): void {
assert(debuggee.id, 'should have a registered debuggee');
): void;

breakpoint.action = 'CAPTURE';
breakpoint.isFinalState = true;
const options = {
uri:
this.apiUrl +
'/debuggees/' +
encodeURIComponent(debuggee.id) +
// TODO: Address the case where `breakpoint.id` is `undefined`.
'/breakpoints/' +
encodeURIComponent(breakpoint.id as string),
json: true,
method: 'PUT',
body: {debuggeeId: debuggee.id, breakpoint},
};
/**
* Start listening to breakpoints updates. The callback will be called when
* there is an unrecoverable error or when the set of active breakpoints has changed.
* @param {!Debuggee} debuggee
* @param {!function(?Error,Object=)} callback accepting (err, breakpoints)
*/
subscribeToBreakpoints(
debuggee: Debuggee,
callback: (err: Error | null, breakpoints: stackdriver.Breakpoint[]) => void
): void;

// We need to have a try/catch here because a JSON.stringify will be done
// by request. Some V8 debug mirror objects get a throw when we attempt to
// stringify them. The try-catch keeps it resilient and avoids crashing the
// user's app.
try {
this.request(options, (err, body /*, response */) => {
callback(err!, body);
});
} catch (error) {
callback(error);
}
}
/**
* Stops the Controller. This is for testing purposes only.
*/
stop(): void;
}
Loading