Skip to content

Commit e160e1a

Browse files
authored
fix: cannot cancel "Scanning CloudFormation templates..." #3987
Problem: Cancelling the CFN registry toast would continue registering YAML files in the background, and merely just hid the popup. Solution: * Exit the for-loop when the Cancel button is clicked. * refactoring: change `registry.rebuild()` to a mandatory call--this makes it easier to pass a cancellation promise. Note: Don't have a way to restart the scan, so if "Cancel" is used, the registry template will be partial or empty until IDE restart.
1 parent b32c4bc commit e160e1a

File tree

7 files changed

+96
-41
lines changed

7 files changed

+96
-41
lines changed

src/codecatalyst/devfile.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ export function registerDevfileWatcher(devenvClient: DevEnvClient): vscode.Dispo
111111
const registry = new DevfileRegistry()
112112
const codelensProvider = new DevfileCodeLensProvider(registry, devenvClient)
113113
registry.addWatchPatterns([devfileGlobPattern])
114+
registry.rebuild()
114115

115116
const codelensDisposable = vscode.languages.registerCodeLensProvider(
116117
{

src/shared/cloudformation/activation.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import * as CloudFormation from './cloudformation'
1515
import { Commands } from '../vscode/commands2'
1616
import globals from '../extensionGlobals'
1717
import { SamCliSettings } from '../sam/cli/samCliSettings'
18+
import { Timeout } from '../utilities/timeoutUtils'
1819

1920
/**
2021
* Creates a CloudFormationTemplateRegistry which retains the state of CloudFormation templates in a workspace.
@@ -56,12 +57,12 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi
5657
* and slowing down the extension starting up.
5758
*/
5859
function setTemplateRegistryInGlobals(registry: CloudFormationTemplateRegistry) {
59-
const registrySetupFunc = async (registry: CloudFormationTemplateRegistry) => {
60-
await registry.addExcludedPattern(CloudFormation.devfileExcludePattern)
61-
await registry.addExcludedPattern(CloudFormation.templateFileExcludePattern)
62-
await registry.addWatchPatterns([CloudFormation.templateFileGlobPattern])
63-
await registry.watchUntitledFiles()
64-
60+
const registrySetupFunc = async (registry: CloudFormationTemplateRegistry, cancellationTimeout: Timeout) => {
61+
registry.addExcludedPattern(CloudFormation.devfileExcludePattern)
62+
registry.addExcludedPattern(CloudFormation.templateFileExcludePattern)
63+
registry.addWatchPatterns([CloudFormation.templateFileGlobPattern])
64+
registry.watchUntitledFiles()
65+
await registry.rebuild(cancellationTimeout)
6566
return registry
6667
}
6768

src/shared/fs/templateRegistry.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { getLambdaDetails } from '../../lambda/utils'
1414
import { WatchedFiles, WatchedItem } from './watchedFiles'
1515
import { getLogger } from '../logger'
1616
import globals from '../extensionGlobals'
17-
import { sleep } from '../utilities/timeoutUtils'
17+
import { Timeout, sleep } from '../utilities/timeoutUtils'
1818
import { localize } from '../utilities/vsCodeUtils'
1919
import { PerfLog } from '../logger/logger'
2020

@@ -71,7 +71,8 @@ export class AsyncCloudFormationTemplateRegistry {
7171
constructor(
7272
private readonly instance: CloudFormationTemplateRegistry,
7373
private readonly asyncSetupFunc: (
74-
instance: CloudFormationTemplateRegistry
74+
instance: CloudFormationTemplateRegistry,
75+
cancelSetup: Timeout
7576
) => Promise<CloudFormationTemplateRegistry>
7677
) {}
7778

@@ -86,15 +87,17 @@ export class AsyncCloudFormationTemplateRegistry {
8687
}
8788

8889
let perf: PerfLog
90+
const cancelSetupPromise = new Timeout(30 * 60 * 1000) // 30 min
8991
if (!this.setupPromise) {
9092
perf = new PerfLog('cfn: template registry setup')
91-
this.setupPromise = this.asyncSetupFunc(this.instance)
93+
this.setupPromise = this.asyncSetupFunc(this.instance, cancelSetupPromise)
9294
}
9395
this.setupPromise.then(() => {
9496
if (perf) {
9597
perf.done()
9698
}
9799
this.isSetup = true
100+
cancelSetupPromise.dispose()
98101
})
99102
// Show user a message indicating setup is in progress
100103
if (this.setupProgressMessage === undefined) {
@@ -111,6 +114,7 @@ export class AsyncCloudFormationTemplateRegistry {
111114
token.onCancellationRequested(() => {
112115
// Allows for new message to be created if templateRegistry variable attempted to be used again
113116
this.setupProgressMessage = undefined
117+
cancelSetupPromise.cancel()
114118
})
115119
getLogger().debug('cfn: getInstance() requested, still initializing')
116120
while (!this.isSetup) {

src/shared/fs/watchedFiles.ts

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import * as path from 'path'
1010
import { globDirs, isUntitledScheme, normalizeVSCodeUri } from '../utilities/vsCodeUtils'
1111
import { Settings } from '../settings'
1212
import { once } from '../utilities/functionUtils'
13+
import { Timeout } from '../utilities/timeoutUtils'
1314

1415
/**
1516
* Prevent `findFiles()` from recursing into these directories.
@@ -60,12 +61,18 @@ const getExcludePatternOnce = once(getExcludePattern)
6061
* for CFN templates among other things. WatchedFiles holds a list of pairs of
6162
* the absolute path to the file or "untitled:" URI along with a transform of it that is useful for
6263
* where it is used. For example, for templates, it parses the template and stores it.
64+
*
65+
* Initialize the registry by adding watch patterns and excluded patterns via
66+
* `addWatchPatterns`, `watchUntitledFiles`, and `addExcludedPattern` and then
67+
* calling the `rebuild` function to begin registering files.
6368
*/
6469
export abstract class WatchedFiles<T> implements vscode.Disposable {
70+
private isWatching: boolean = false
6571
private readonly disposables: vscode.Disposable[] = []
6672
private _isDisposed: boolean = false
6773
private readonly globs: vscode.GlobPattern[] = []
6874
private readonly excludedFilePatterns: RegExp[] = []
75+
private watchingUntitledFiles: boolean = false
6976
private readonly registryData: Map<string, T> = new Map<string, T>()
7077

7178
/**
@@ -93,7 +100,7 @@ export abstract class WatchedFiles<T> implements vscode.Disposable {
93100
* Creates watchers for each glob across all opened workspace folders (or see below to
94101
* watch _outside_ the workspace).
95102
*
96-
* Fails if the watcher is disposed, or if globs have already been set;
103+
* Fails if the watcher is disposed, or if the watcher has been initialized through the `rebuild` function;
97104
* enforce setting once to reduce rebuilds looping through all existing globs
98105
*
99106
* (since vscode 1.64):
@@ -120,12 +127,12 @@ export abstract class WatchedFiles<T> implements vscode.Disposable {
120127
*
121128
* @param globs Patterns to match against (across all opened workspace folders)
122129
*/
123-
public async addWatchPatterns(globs: vscode.GlobPattern[]): Promise<void> {
130+
public addWatchPatterns(globs: vscode.GlobPattern[]) {
124131
if (this._isDisposed) {
125132
throw new Error(`${this.name}: manager has already been disposed!`)
126133
}
127-
if (this.globs.length > 0) {
128-
throw new Error(`${this.name}: watch patterns have already been established`)
134+
if (this.isWatching) {
135+
throw new Error(`${this.name}: cannot add watch patterns after watcher has begun watching`)
129136
}
130137
for (const glob of globs) {
131138
if (typeof glob === 'string' && !vscode.workspace.workspaceFolders?.[0]) {
@@ -136,15 +143,14 @@ export abstract class WatchedFiles<T> implements vscode.Disposable {
136143
const watcher = vscode.workspace.createFileSystemWatcher(glob)
137144
this.addWatcher(watcher)
138145
}
139-
140-
await this.rebuild()
141146
}
142147

143148
/**
144149
* Create a special watcher that operates only on untitled files.
145150
* To "watch" the in-memory contents of an untitled:/ file we just subscribe to `onDidChangeTextDocument`
146151
*/
147-
public async watchUntitledFiles() {
152+
public watchUntitledFiles() {
153+
this.watchingUntitledFiles = true
148154
this.disposables.push(
149155
vscode.workspace.onDidChangeTextDocument((event: vscode.TextDocumentChangeEvent) => {
150156
if (isUntitledScheme(event.document.uri)) {
@@ -162,13 +168,14 @@ export abstract class WatchedFiles<T> implements vscode.Disposable {
162168
/**
163169
* Adds a regex pattern to ignore paths containing the pattern
164170
*/
165-
public async addExcludedPattern(pattern: RegExp): Promise<void> {
171+
public addExcludedPattern(pattern: RegExp) {
166172
if (this._isDisposed) {
167173
throw new Error(`${this.name}: manager has already been disposed!`)
168174
}
175+
if (this.isWatching) {
176+
throw new Error(`${this.name}: cannot add excluded patterns after watcher has begun watching`)
177+
}
169178
this.excludedFilePatterns.push(pattern)
170-
171-
await this.rebuild()
172179
}
173180

174181
/**
@@ -275,26 +282,37 @@ export abstract class WatchedFiles<T> implements vscode.Disposable {
275282
}
276283

277284
/**
278-
* Rebuilds registry using current glob and exclusion patterns.
279-
* All functionality is currently internal to class, but can be made public if we want a manual "refresh" button
285+
* Builds/rebuilds registry using current glob and exclusion patterns. ***Necessary to init registry***.
286+
*
287+
* @param cancellationTimeout Optional timeout that can trigger canceling additional loading on completion (manual or timed)
280288
*/
281-
public async rebuild(): Promise<void> {
289+
public async rebuild(cancellationTimeout?: Timeout): Promise<void> {
290+
let skips = 0
291+
this.isWatching = true
282292
this.reset()
283293

284294
const exclude = getExcludePatternOnce()
285-
for (const glob of this.globs) {
295+
getLogger().info(`${this.name}: Building registry with the following criteria: ${this.outputPatterns()}`)
296+
for (let i = 0; i < this.globs.length && !cancellationTimeout?.completed; i++) {
297+
const glob = this.globs[i]
286298
try {
287299
const found = await vscode.workspace.findFiles(glob, exclude)
288-
for (const item of found) {
289-
await this.addItem(item, true)
300+
for (let j = 0; j < found.length && !cancellationTimeout?.completed; j++) {
301+
await this.addItem(found[j], true)
290302
}
291303
} catch (e) {
304+
// file not processable
305+
skips++
292306
const err = e as Error
293-
if (err.name !== 'Canceled') {
294-
getLogger().error('watchedFiles: findFiles("%s", "%s"): %s', glob, exclude, err.message)
295-
}
307+
getLogger().error(`${this.name}: watchedFiles: findFiles("%s", "%s"): %s`, glob, exclude, err.message)
296308
}
297309
}
310+
getLogger().info(
311+
`${this.name}: Registered %s items%s%s`,
312+
this.registryData.size,
313+
cancellationTimeout?.completed ? ' (cancelled)' : '',
314+
skips > 0 ? `, skipped ${skips} entries` : ''
315+
)
298316
}
299317

300318
/**
@@ -340,6 +358,18 @@ export abstract class WatchedFiles<T> implements vscode.Disposable {
340358
throw new Error(`FileRegistry: path is relative when it should be absolute: ${pathAsString}`)
341359
}
342360
}
361+
362+
private outputPatterns(): string {
363+
const watch = this.globs.map(cur => cur.toString())
364+
if (this.watchingUntitledFiles) {
365+
watch.push('Untitled Files')
366+
}
367+
const exclude = this.excludedFilePatterns.map(cur => cur.toString())
368+
369+
return `${watch.length > 0 ? `Watching ${watch.join(', ')}` : ''}${
370+
exclude.length > 0 ? `, Excluding ${exclude.join(', ')}` : ''
371+
}`
372+
}
343373
}
344374

345375
export class NoopWatcher extends WatchedFiles<any> {

src/shared/sam/activation.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,14 +170,15 @@ async function activateCodeLensRegistry(context: ExtContext) {
170170
// "**/…" string patterns watch recursively across _all_ workspace
171171
// folders (see documentation for addWatchPatterns()).
172172
//
173-
await registry.addWatchPatterns([
173+
registry.addWatchPatterns([
174174
pyLensProvider.pythonBasePattern,
175175
jsLensProvider.javascriptBasePattern,
176176
csLensProvider.csharpBasePattern,
177177
goLensProvider.goBasePattern,
178178
javaLensProvider.gradleBasePattern,
179179
javaLensProvider.mavenBasePattern,
180180
])
181+
await registry.rebuild()
181182
} catch (e) {
182183
vscode.window.showErrorMessage(
183184
localize(

src/test/shared/debug/launchConfiguration.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,8 @@ describe('LaunchConfiguration', function () {
106106

107107
beforeEach(async function () {
108108
const registry = await globals.templateRegistry
109-
await registry.addWatchPatterns([CloudFormation.templateFileGlobPattern])
109+
registry.addWatchPatterns([CloudFormation.templateFileGlobPattern])
110+
await registry.rebuild()
110111

111112
// TODO: remove mocks in favor of testing src/testFixtures/ data.
112113
mockConfigSource = mock()

src/testInteg/cloudformation/templateRegistry.test.ts

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import * as fs from 'fs-extra'
99
import { CloudFormationTemplateRegistry } from '../../shared/fs/templateRegistry'
1010
import { makeSampleSamTemplateYaml, strToYamlFile } from '../../test/shared/cloudformation/cloudformationTestUtils'
1111
import { getTestWorkspaceFolder } from '../integrationTestsUtilities'
12-
import { sleep, waitUntil } from '../../shared/utilities/timeoutUtils'
12+
import { Timeout, sleep, waitUntil } from '../../shared/utilities/timeoutUtils'
1313
import assert from 'assert'
1414

1515
/**
@@ -44,13 +44,15 @@ describe('CloudFormation Template Registry', async function () {
4444
await strToYamlFile(makeSampleSamTemplateYaml(true), path.join(testDir, 'test.yaml'))
4545
await strToYamlFile(makeSampleSamTemplateYaml(false), path.join(testDirNested, 'test.yml'))
4646

47-
await registry.addWatchPatterns(['**/test.{yaml,yml}'])
47+
registry.addWatchPatterns(['**/test.{yaml,yml}'])
48+
await registry.rebuild()
4849

4950
await registryHasTargetNumberOfFiles(registry, 2)
5051
})
5152

5253
it.skip('adds dynamically-added template files with yaml and yml extensions at various nesting levels', async function () {
53-
await registry.addWatchPatterns(['**/test.{yaml,yml}'])
54+
registry.addWatchPatterns(['**/test.{yaml,yml}'])
55+
await registry.rebuild()
5456

5557
await strToYamlFile(makeSampleSamTemplateYaml(false), path.join(testDir, 'test.yml'))
5658
await strToYamlFile(makeSampleSamTemplateYaml(true), path.join(testDirNested, 'test.yaml'))
@@ -59,8 +61,9 @@ describe('CloudFormation Template Registry', async function () {
5961
})
6062

6163
it('Ignores templates matching excluded patterns', async function () {
62-
await registry.addWatchPatterns(['**/test.{yaml,yml}'])
63-
await registry.addExcludedPattern(/.*nested.*/)
64+
registry.addWatchPatterns(['**/test.{yaml,yml}'])
65+
registry.addExcludedPattern(/.*nested.*/)
66+
await registry.rebuild()
6467

6568
await strToYamlFile(makeSampleSamTemplateYaml(false), path.join(testDir, 'test.yml'))
6669
await strToYamlFile(makeSampleSamTemplateYaml(true), path.join(testDirNested, 'test.yaml'))
@@ -72,7 +75,8 @@ describe('CloudFormation Template Registry', async function () {
7275
const filepath = path.join(testDir, 'changeMe.yml')
7376
await strToYamlFile(makeSampleSamTemplateYaml(false), filepath)
7477

75-
await registry.addWatchPatterns(['**/changeMe.yml'])
78+
registry.addWatchPatterns(['**/changeMe.yml'])
79+
await registry.rebuild()
7680

7781
await registryHasTargetNumberOfFiles(registry, 1)
7882

@@ -84,7 +88,8 @@ describe('CloudFormation Template Registry', async function () {
8488
})
8589

8690
it('can handle deleted files', async function () {
87-
await registry.addWatchPatterns(['**/deleteMe.yml'])
91+
registry.addWatchPatterns(['**/deleteMe.yml'])
92+
await registry.rebuild()
8893

8994
// Specifically creating the file after the watcher is added
9095
// Otherwise, it seems the file is deleted before the file watcher realizes the file exists
@@ -99,11 +104,23 @@ describe('CloudFormation Template Registry', async function () {
99104
await registryHasTargetNumberOfFiles(registry, 0)
100105
})
101106

102-
it('fails if you set watch patterns multiple times', async function () {
103-
await registry.addWatchPatterns(['first/set'])
107+
it('fails if you set watch patterns after init', async function () {
108+
registry.addWatchPatterns(['first/set'])
109+
await registry.rebuild()
104110
await assert.rejects(async () => {
105-
await registry.addWatchPatterns(['second/set'])
106-
}, new Error('CloudFormationTemplateRegistry: watch patterns have already been established'))
111+
registry.addWatchPatterns(['second/set'])
112+
}, new Error('CloudFormationTemplateRegistry: cannot add watch patterns after watcher has begun watching'))
113+
await assert.rejects(async () => {
114+
registry.addExcludedPattern(/exclude me/)
115+
}, new Error('CloudFormationTemplateRegistry: cannot add excluded patterns after watcher has begun watching'))
116+
})
117+
118+
it('cancels the rebuild if the cancellation timeout is completed', async function () {
119+
registry.addWatchPatterns(['**/test.{yaml,yml}'])
120+
const t = new Timeout(100)
121+
t.cancel()
122+
await registry.rebuild(t)
123+
await registryHasTargetNumberOfFiles(registry, 0)
107124
})
108125
})
109126

0 commit comments

Comments
 (0)