diff --git a/src/agent/debuglet.ts b/src/agent/debuglet.ts index 255f337e..328ad47e 100644 --- a/src/agent/debuglet.ts +++ b/src/agent/debuglet.ts @@ -187,6 +187,7 @@ export interface FindFilesResult { export class Debuglet extends EventEmitter { private debug: Debug; private v8debug: DebugApi | null; + private started: boolean; private running: boolean; private project: string | null; private controller: Controller | null; @@ -241,6 +242,9 @@ export class Debuglet extends EventEmitter { */ this.v8debug = null; + /** @private {boolean} */ + this.started = false; + /** @private {boolean} */ this.running = false; @@ -351,6 +355,7 @@ export class Debuglet extends EventEmitter { * @private */ async start(): Promise { + this.started = true; const stat = util.promisify(fs.stat); try { @@ -734,6 +739,10 @@ export class Debuglet extends EventEmitter { } setTimeout(() => { + if (!this.running) { + this.logger.info('Debuglet is stopped; not registering'); + return; + } assert(that.controller); if (!that.running) { onError(new Error('Debuglet not running')); @@ -785,6 +794,10 @@ export class Debuglet extends EventEmitter { } startListeningForBreakpoints_(): void { + if (!this.running) { + this.logger.info('Debuglet is stopped; not listening for breakpoints'); + return; + } assert(this.controller); // TODO: Handle the case where this.debuggee is null or not properly registered. this.controller.subscribeToBreakpoints( @@ -1072,16 +1085,30 @@ export class Debuglet extends EventEmitter { } /** - * Stops the Debuglet. This is for testing purposes only. Stop should only be - * called on a agent that has started (i.e. emitted the 'started' event). - * Calling this while the agent is initializing may not necessarily stop all - * pending operations. + * Stops the Debuglet. + * + * Stop should only be called on a agent that has started. */ stop(): void { + if (this.running) { + this.stopController(); + } else { + if (!this.started) { + this.logger.info('Attempt to stop Debuglet before it was started'); + return; + } + this.on('started', () => { + this.stopController(); + }); + } + } + + stopController(): void { assert(this.controller); assert.ok(this.running, 'stop can only be called on a running agent'); this.logger.debug('Stopping Debuglet'); this.running = false; + this.started = false; this.controller.stop(); this.emit('stopped'); } diff --git a/src/agent/firebase-controller.ts b/src/agent/firebase-controller.ts index 455b43d9..4e0e61aa 100644 --- a/src/agent/firebase-controller.ts +++ b/src/agent/firebase-controller.ts @@ -349,7 +349,7 @@ export class FirebaseController implements Controller { debuglog(`starting to mark every ${this.markActivePeriodMsec} ms`); this.markActiveInterval = setInterval(() => { this.markDebuggeeActive(); - }, this.markActivePeriodMsec); + }, this.markActivePeriodMsec).unref(); } /** diff --git a/src/index.ts b/src/index.ts index 8d2c0858..97697fa7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,7 +23,7 @@ import * as util from 'util'; const debuglog = util.debuglog('cdbg'); // Singleton. -let debuglet: Debuglet; +let debuglet: Debuglet | undefined; /** * Start the Debug agent that will make your application available for debugging @@ -86,3 +86,14 @@ function mergeConfigs(options: T & {debug?: T}): T { export function get(): Debuglet | undefined { return debuglet; } + +/** + * Cleanly shut down the debug agent. + * + * This will free up all resources. It may be necessary to call this to allow + * your process to shut down cleanly. + */ +export function stop(): void { + debuglet?.stop(); + debuglet = undefined; +} diff --git a/system-test/fixtures/sample/src/allowExpressions.ts b/system-test/fixtures/sample/src/allowExpressions.ts index 70bc9eb1..825b2a0b 100644 --- a/system-test/fixtures/sample/src/allowExpressions.ts +++ b/system-test/fixtures/sample/src/allowExpressions.ts @@ -13,3 +13,4 @@ import * as debug from '@google-cloud/debug-agent'; debug.start({allowExpressions: true}); +debug.stop(); diff --git a/system-test/fixtures/sample/src/allowExpressionsJs.js b/system-test/fixtures/sample/src/allowExpressionsJs.js index 3623336b..63a5f8ce 100644 --- a/system-test/fixtures/sample/src/allowExpressionsJs.js +++ b/system-test/fixtures/sample/src/allowExpressionsJs.js @@ -14,3 +14,4 @@ require('@google-cloud/debug-agent').start({ allowExpressions: true, }); +require('@google-cloud/debug-agent').stop(); diff --git a/system-test/fixtures/sample/src/completeServiceContext.ts b/system-test/fixtures/sample/src/completeServiceContext.ts index 76a4a722..980837d6 100644 --- a/system-test/fixtures/sample/src/completeServiceContext.ts +++ b/system-test/fixtures/sample/src/completeServiceContext.ts @@ -19,3 +19,4 @@ debug.start({ version: 'Some version', }, }); +debug.stop(); diff --git a/system-test/fixtures/sample/src/firebase.ts b/system-test/fixtures/sample/src/firebase.ts new file mode 100644 index 00000000..daefcd7d --- /dev/null +++ b/system-test/fixtures/sample/src/firebase.ts @@ -0,0 +1,16 @@ +// Copyright 2023 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as debug from '@google-cloud/debug-agent'; +debug.start({useFirebase: true}); +debug.stop(); diff --git a/system-test/fixtures/sample/src/import.ts b/system-test/fixtures/sample/src/import.ts index 6bd50d01..73717ee4 100644 --- a/system-test/fixtures/sample/src/import.ts +++ b/system-test/fixtures/sample/src/import.ts @@ -13,3 +13,4 @@ import * as debug from '@google-cloud/debug-agent'; debug.start(); +debug.stop(); diff --git a/system-test/fixtures/sample/src/noargs.ts b/system-test/fixtures/sample/src/noargs.ts index 6bd50d01..73717ee4 100644 --- a/system-test/fixtures/sample/src/noargs.ts +++ b/system-test/fixtures/sample/src/noargs.ts @@ -13,3 +13,4 @@ import * as debug from '@google-cloud/debug-agent'; debug.start(); +debug.stop(); diff --git a/system-test/fixtures/sample/src/partialCapture.ts b/system-test/fixtures/sample/src/partialCapture.ts index 54b45dca..034045bc 100644 --- a/system-test/fixtures/sample/src/partialCapture.ts +++ b/system-test/fixtures/sample/src/partialCapture.ts @@ -17,3 +17,4 @@ debug.start({ maxFrames: 1, }, }); +debug.stop(); diff --git a/system-test/fixtures/sample/src/partialServiceContext.ts b/system-test/fixtures/sample/src/partialServiceContext.ts index bb9480c7..10202e2f 100644 --- a/system-test/fixtures/sample/src/partialServiceContext.ts +++ b/system-test/fixtures/sample/src/partialServiceContext.ts @@ -18,3 +18,4 @@ debug.start({ service: 'Some service', }, }); +debug.stop(); diff --git a/system-test/fixtures/sample/src/start.js b/system-test/fixtures/sample/src/start.js index eb43d997..3c25ac83 100644 --- a/system-test/fixtures/sample/src/start.js +++ b/system-test/fixtures/sample/src/start.js @@ -12,3 +12,4 @@ // limitations under the License. require('@google-cloud/debug-agent').start(); +require('@google-cloud/debug-agent').stop(); diff --git a/system-test/fixtures/sample/src/startEmpty.js b/system-test/fixtures/sample/src/startEmpty.js index a4ae5ba3..99333013 100644 --- a/system-test/fixtures/sample/src/startEmpty.js +++ b/system-test/fixtures/sample/src/startEmpty.js @@ -12,3 +12,4 @@ // limitations under the License. require('@google-cloud/debug-agent').start({}); +require('@google-cloud/debug-agent').stop(); diff --git a/test/test-debuglet.ts b/test/test-debuglet.ts index c8b7f152..edb207c1 100644 --- a/test/test-debuglet.ts +++ b/test/test-debuglet.ts @@ -840,6 +840,49 @@ describe('Debuglet', () => { debuglet.start(); }); + it('should stop successfully even if stop is called quickly', () => { + const debug = new Debug( + {projectId: 'fake-project', credentials: fakeCredentials}, + packageInfo + ); + + nocks.oauth2(); + + const config = debugletConfig(); + const debuglet = new Debuglet(debug, config); + + debuglet.start(); + debuglet.stop(); + }); + + it('should register successfully even if stop was called first', done => { + const debug = new Debug( + {projectId: 'fake-project', credentials: fakeCredentials}, + packageInfo + ); + + nocks.oauth2(); + + const config = debugletConfig(); + const debuglet = new Debuglet(debug, config); + const scope = nock(config.apiUrl) + .post(REGISTER_PATH) + .reply(200, { + debuggee: {id: DEBUGGEE_ID}, + }); + + debuglet.once('registered', (id: string) => { + assert.strictEqual(id, DEBUGGEE_ID); + debuglet.stop(); + scope.done(); + done(); + }); + + debuglet.stop(); + + debuglet.start(); + }); + it('should attempt to retrieve cluster name if needed', done => { const savedRunningOnGCP = Debuglet.runningOnGCP; Debuglet.runningOnGCP = () => {