Skip to content

Commit f9a84e9

Browse files
refactor: simplify folder logic
There was some excessive code due to an older design. This removes the use of nested folders in favor of using the root folder to store state. This simplifies the code. Signed-off-by: nkomonen-amazon <[email protected]>
1 parent 8340d18 commit f9a84e9

File tree

3 files changed

+45
-91
lines changed

3 files changed

+45
-91
lines changed

packages/core/src/shared/constants.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -164,10 +164,6 @@ export const amazonQVscodeMarketplace =
164164
*
165165
* Moved here to resolve circular dependency issues.
166166
*/
167-
export const crashMonitoringDirNames = {
168-
root: 'crashMonitoring',
169-
running: 'running',
170-
shutdown: 'shutdown',
171-
} as const
167+
export const crashMonitoringDirName = 'crashMonitoring'
172168

173169
export const amazonQTabSuffix = '(Generated by Amazon Q)'

packages/core/src/shared/crashMonitoring.ts

Lines changed: 42 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { isNewOsSession } from './utilities/osUtils'
1515
import nodeFs from 'fs/promises'
1616
import fs from './fs/fs'
1717
import { getLogger } from './logger/logger'
18-
import { crashMonitoringDirNames } from './constants'
18+
import { crashMonitoringDirName } from './constants'
1919
import { throwOnUnstableFileSystem } from './filesystemUtilities'
2020
import { withRetries } from './utilities/functionUtils'
2121
import { TimeLag } from './utilities/timeoutUtils'
@@ -307,35 +307,26 @@ class CrashChecker {
307307
continue
308308
}
309309

310-
// Ext is not running anymore, handle appropriately depending on why it stopped running
311-
await state.handleExtNotRunning(ext, {
312-
onShutdown: async () => {
313-
// Nothing to do, just log info if necessary
314-
devLogger?.debug(
315-
`crashMonitoring: SHUTDOWN: following has gracefully shutdown: pid ${ext.extHostPid} + sessionId: ${ext.sessionId}`
316-
)
317-
},
318-
onCrash: async () => {
319-
// Debugger instances may incorrectly look like they crashed, so don't emit.
320-
// Example is if I hit the red square in the debug menu, it is a non-graceful shutdown. But the regular
321-
// 'x' button in the Debug IDE instance is a graceful shutdown.
322-
if (ext.isDebug) {
323-
devLogger?.debug(`crashMonitoring: DEBUG instance crashed: ${JSON.stringify(ext)}`)
324-
return
325-
}
326-
327-
// This is the metric to let us know the extension crashed
328-
telemetry.session_end.emit({
329-
result: 'Failed',
330-
proxiedSessionId: ext.sessionId,
331-
reason: 'ExtHostCrashed',
332-
passive: true,
333-
})
334-
335-
devLogger?.debug(
336-
`crashMonitoring: CRASH: following has crashed: pid ${ext.extHostPid} + sessionId: ${ext.sessionId}`
337-
)
338-
},
310+
await state.handleCrashedExt(ext, () => {
311+
// Debugger instances may incorrectly look like they crashed, so don't emit.
312+
// Example is if I hit the red square in the debug menu, it is a non-graceful shutdown. But the regular
313+
// 'x' button in the Debug IDE instance is a graceful shutdown.
314+
if (ext.isDebug) {
315+
devLogger?.debug(`crashMonitoring: DEBUG instance crashed: ${JSON.stringify(ext)}`)
316+
return
317+
}
318+
319+
// This is the metric to let us know the extension crashed
320+
telemetry.session_end.emit({
321+
result: 'Failed',
322+
proxiedSessionId: ext.sessionId,
323+
reason: 'ExtHostCrashed',
324+
passive: true,
325+
})
326+
327+
devLogger?.debug(
328+
`crashMonitoring: CRASH: following has crashed: pid ${ext.extHostPid} + sessionId: ${ext.sessionId}`
329+
)
339330
})
340331
}
341332

@@ -421,7 +412,7 @@ export class FileSystemState {
421412
* IMORTANT: Use {@link crashMonitoringStateFactory} to make an instance
422413
*/
423414
constructor(protected readonly deps: MementoStateDependencies) {
424-
this.stateDirPath = path.join(this.deps.workDirPath, crashMonitoringDirNames.root)
415+
this.stateDirPath = path.join(this.deps.workDirPath, crashMonitoringDirName)
425416

426417
this.deps.devLogger?.debug(`crashMonitoring: pid: ${this.deps.pid}`)
427418
this.deps.devLogger?.debug(`crashMonitoring: sessionId: ${this.deps.sessionId.slice(0, 8)}-...`)
@@ -447,17 +438,16 @@ export class FileSystemState {
447438
if (await this.deps.isStateStale()) {
448439
await this.clearState()
449440
}
441+
442+
await withFailCtx('init', () => fs.mkdir(this.stateDirPath))
450443
}
451444

452445
// ------------------ Heartbeat methods ------------------
453446
public async sendHeartbeat() {
454-
const extId = this.createExtId(this.ext)
455-
456447
try {
457448
const func = async () => {
458-
const dir = await this.runningExtsDir()
459449
await fs.writeFile(
460-
path.join(dir, extId),
450+
this.makeStateFilePath(this.extId),
461451
JSON.stringify({ ...this.ext, lastHeartbeat: this.deps.now() }, undefined, 4)
462452
)
463453
this.deps.devLogger?.debug(
@@ -479,7 +469,7 @@ export class FileSystemState {
479469
}
480470

481471
/**
482-
* Signal that this extension is gracefully shutting down. This will prevent the IDE from thinking it crashed.
472+
* Indicates that this extension instance has gracefully shutdown.
483473
*
484474
* IMPORTANT: This code is being run in `deactivate()` where VS Code api is not available. Due to this we cannot
485475
* easily update the state to indicate a graceful shutdown. So the next best option is to write to a file on disk,
@@ -489,46 +479,24 @@ export class FileSystemState {
489479
* function touches.
490480
*/
491481
public async indicateGracefulShutdown(): Promise<void> {
492-
const dir = await this.shutdownExtsDir()
493-
await withFailCtx('writeShutdownFile', () => nodeFs.writeFile(path.join(dir, this.extId), ''))
482+
// By removing the heartbeat entry, the crash checkers will not be able to find this entry anymore, making it
483+
// impossible to report on since the file system is the source of truth
484+
await withFailCtx('indicateGracefulShutdown', () => nodeFs.rm(this.makeStateFilePath(this.extId)))
494485
}
495486

496487
// ------------------ Checker Methods ------------------
497488

498-
/**
499-
* Signals the state that the given extension is not running, allowing the state to appropriately update
500-
* depending on a graceful shutdown or crash.
501-
*
502-
* NOTE: This does NOT run in the `deactivate()` method, so it CAN reliably use the VS Code FS api
503-
*
504-
* @param opts - functions to run depending on why the extension stopped running
505-
*/
506-
public async handleExtNotRunning(
507-
ext: ExtInstance,
508-
opts: { onShutdown: () => Promise<void>; onCrash: () => Promise<void> }
509-
): Promise<void> {
510-
const extId = this.createExtId(ext)
511-
const shutdownFilePath = path.join(await this.shutdownExtsDir(), extId)
512-
513-
if (await withFailCtx('existsShutdownFile', () => fs.exists(shutdownFilePath))) {
514-
await opts.onShutdown()
515-
// We intentionally do not clean up the file in shutdown since there may be another
516-
// extension may be doing the same thing in parallel, and would read the extension as
517-
// crashed since the file was missing. The file will be cleared on computer restart though.
518-
519-
// TODO: Be smart and clean up the file after some time.
520-
} else {
521-
await opts.onCrash()
522-
}
523-
524-
// Clean up the running extension file since it no longer exists
525-
await this.deleteHeartbeatFile(extId)
489+
public async handleCrashedExt(ext: ExtInstance, fn: () => void) {
490+
await withFailCtx('handleCrashedExt', async () => {
491+
await this.deleteHeartbeatFile(ext)
492+
fn()
493+
})
526494
}
527-
public async deleteHeartbeatFile(extId: ExtInstanceId) {
528-
const dir = await this.runningExtsDir()
495+
496+
private async deleteHeartbeatFile(ext: ExtInstanceId | ExtInstance) {
529497
// Retry file deletion to prevent incorrect crash reports. Common Windows errors seen in telemetry: EPERM/EBUSY.
530498
// See: https://github.com/aws/aws-toolkit-vscode/pull/5335
531-
await withRetries(() => withFailCtx('deleteStaleRunningFile', () => fs.delete(path.join(dir, extId))), {
499+
await withRetries(() => withFailCtx('deleteStaleRunningFile', () => fs.delete(this.makeStateFilePath(ext))), {
532500
maxRetries: 8,
533501
delay: 100,
534502
backoff: 2,
@@ -557,17 +525,9 @@ export class FileSystemState {
557525
protected createExtId(ext: ExtInstance): ExtInstanceId {
558526
return `${ext.extHostPid}_${ext.sessionId}`
559527
}
560-
private async runningExtsDir(): Promise<string> {
561-
const p = path.join(this.stateDirPath, crashMonitoringDirNames.running)
562-
// ensure the dir exists
563-
await withFailCtx('ensureRunningExtsDir', () => nodeFs.mkdir(p, { recursive: true }))
564-
return p
565-
}
566-
private async shutdownExtsDir() {
567-
const p = path.join(this.stateDirPath, crashMonitoringDirNames.shutdown)
568-
// Since this runs in `deactivate()` it cannot use the VS Code FS api
569-
await withFailCtx('ensureShutdownExtsDir', () => nodeFs.mkdir(p, { recursive: true }))
570-
return p
528+
private makeStateFilePath(ext: ExtInstance | ExtInstanceId) {
529+
const extId = typeof ext === 'string' ? ext : this.createExtId(ext)
530+
return path.join(this.stateDirPath, extId)
571531
}
572532
public async clearState(): Promise<void> {
573533
this.deps.devLogger?.debug('crashMonitoring: CLEAR_STATE: Started')
@@ -580,7 +540,7 @@ export class FileSystemState {
580540
const res = await withFailCtx('getAllExts', async () => {
581541
// The file names are intentionally the IDs for easy mapping
582542
const allExtIds: ExtInstanceId[] = await withFailCtx('readdir', async () =>
583-
(await fs.readdir(await this.runningExtsDir())).map((k) => k[0])
543+
(await fs.readdir(this.stateDirPath)).map((k) => k[0])
584544
)
585545

586546
const allExts = allExtIds.map<Promise<ExtInstanceHeartbeat | undefined>>(async (extId: string) => {
@@ -591,7 +551,7 @@ export class FileSystemState {
591551
() =>
592552
withFailCtx('parseRunningExtFile', async () =>
593553
ignoreBadFileError(async () => {
594-
const text = await fs.readFileText(path.join(await this.runningExtsDir(), extId))
554+
const text = await fs.readFileText(this.makeStateFilePath(extId))
595555

596556
if (!text) {
597557
return undefined

packages/core/src/shared/errors.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import type * as os from 'os'
1515
import { CodeWhispererStreamingServiceException } from '@amzn/codewhisperer-streaming'
1616
import { driveLetterRegex } from './utilities/pathUtils'
1717
import { getLogger } from './logger/logger'
18-
import { crashMonitoringDirNames } from './constants'
18+
import { crashMonitoringDirName } from './constants'
1919

2020
let _username = 'unknown-user'
2121
let _isAutomation = false
@@ -397,9 +397,7 @@ export function scrubNames(s: string, username?: string) {
397397
'tmp',
398398
'aws-toolkit-vscode',
399399
'globalStorage', // from vscode globalStorageUri
400-
crashMonitoringDirNames.root,
401-
crashMonitoringDirNames.running,
402-
crashMonitoringDirNames.shutdown,
400+
crashMonitoringDirName,
403401
])
404402

405403
if (username && username.length > 2) {

0 commit comments

Comments
 (0)