Skip to content

Commit ef119b7

Browse files
committed
fix(amazonq): do not trigger uninstall when extension auto-updates
1 parent 8831e6b commit ef119b7

File tree

2 files changed

+90
-32
lines changed

2 files changed

+90
-32
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "Bug Fix",
3+
"description": "do not trigger uninstall event when the extension auto-updates"
4+
}

packages/core/src/shared/handleUninstall.ts

Lines changed: 86 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -7,59 +7,113 @@
77

88
import * as vscode from 'vscode'
99
import { existsSync } from 'fs'
10+
import * as semver from 'semver'
1011
import { join } from 'path'
1112
import { getLogger } from './logger/logger'
1213
import { telemetry } from './telemetry'
1314
import { VSCODE_EXTENSION_ID } from './extensions'
1415
import { extensionVersion } from './vscode/env'
1516

1617
/**
17-
* Checks if the extension has been uninstalled by reading the .obsolete file
18-
* and comparing the number of obsolete extensions with the installed extensions.
18+
* Checks if an extension has been uninstalled and performs a callback if so.
19+
* This function differentiates between an uninstall and an auto-update.
1920
*
20-
* @param {string} extensionName - The name of the extension.
21-
* @param {string} extensionsDirPath - The path to the extensions directory.
22-
* @param {string} obsoleteFilePath - The path to the .obsolete file.
23-
* @param {function} callback - Action performed when extension is uninstalled.
24-
* @returns {void}
21+
* @param extensionId - The ID of the extension to check (e.g., VSCODE_EXTENSION_ID.awstoolkit)
22+
* @param extensionsPath - The file system path to the VS Code extensions directory
23+
* @param obsoletePath - The file system path to the .obsolete file
24+
* @param onUninstallCallback - A callback function to execute if the extension is uninstalled
2525
*/
2626
async function checkExtensionUninstall(
27-
extensionName: typeof VSCODE_EXTENSION_ID.awstoolkit | typeof VSCODE_EXTENSION_ID.amazonq,
28-
extensionsDirPath: string,
29-
obsoleteFilePath: string,
30-
callback: () => Promise<void>
27+
extensionId: typeof VSCODE_EXTENSION_ID.awstoolkit | typeof VSCODE_EXTENSION_ID.amazonq,
28+
extensionsPath: string,
29+
obsoletePath: string,
30+
onUninstallCallback: () => Promise<void>
3131
): Promise<void> {
32-
/**
33-
* Users can have multiple profiles with different versions of the extensions.
34-
*
35-
* This makes sure the callback is triggered only when an explicit extension with specific version is uninstalled.
36-
*/
37-
const extension = `${extensionName}-${extensionVersion}`
32+
const extensionFullName = `${extensionId}-${extensionVersion}`
33+
3834
try {
39-
const [obsoleteFileContent, extensionsDirContent] = await Promise.all([
40-
vscode.workspace.fs.readFile(vscode.Uri.file(obsoleteFilePath)),
41-
vscode.workspace.fs.readDirectory(vscode.Uri.file(extensionsDirPath)),
35+
const [obsoleteFileContent, extensionDirEntries] = await Promise.all([
36+
vscode.workspace.fs.readFile(vscode.Uri.file(obsoletePath)),
37+
vscode.workspace.fs.readDirectory(vscode.Uri.file(extensionsPath)),
4238
])
4339

44-
const installedExtensionsCount = extensionsDirContent
45-
.map(([name]) => name)
46-
.filter((name) => name.includes(extension)).length
47-
4840
const obsoleteExtensions = JSON.parse(obsoleteFileContent.toString())
49-
const obsoleteExtensionsCount = Object.keys(obsoleteExtensions).filter((id) => id.includes(extension)).length
50-
51-
if (installedExtensionsCount === obsoleteExtensionsCount) {
52-
await callback()
53-
telemetry.aws_extensionUninstalled.run((span) => {
54-
span.record({})
55-
})
56-
getLogger().info(`UninstallExtension: ${extension} uninstalled successfully`)
41+
const currentExtension = vscode.extensions.getExtension(extensionId)
42+
43+
if (!currentExtension) {
44+
// Check if the extension was previously installed and is now in the obsolete list
45+
const wasObsolete = Object.keys(obsoleteExtensions).some((id) => id.startsWith(extensionId))
46+
if (wasObsolete) {
47+
await handleUninstall(extensionFullName, onUninstallCallback)
48+
}
49+
} else {
50+
// Check if there's a newer version in the extensions directory
51+
const newerVersionExists = checkForNewerVersion(
52+
extensionDirEntries,
53+
extensionId,
54+
currentExtension.packageJSON.version
55+
)
56+
57+
if (!newerVersionExists) {
58+
// No newer version exists, so this is likely an uninstall
59+
await handleUninstall(extensionFullName, onUninstallCallback)
60+
} else {
61+
getLogger().info(`UpdateExtension: ${extensionFullName} is being updated`)
62+
}
5763
}
5864
} catch (error) {
5965
getLogger().error(`UninstallExtension: Failed to check .obsolete: ${error}`)
6066
}
6167
}
6268

69+
/**
70+
* Checks if a newer version of the extension exists in the extensions directory.
71+
* The isExtensionInstalled fn is used to determine if the extension is installed using the vscode API
72+
* whereas this function checks for the newer version in the extension directory for scenarios where
73+
* the old extension is un-installed and the new extension in downloaded but not installed.
74+
*
75+
* @param onUninstallCallback - A callback function to execute if the extension is uninstalled
76+
* @param isExtensionInstalled - A function to check if the extension is installed
77+
* @param dirEntries - The entries in the extensions directory
78+
* @param extensionId - The ID of the extension to check
79+
* @param currentVersion - The current version of the extension
80+
* @returns True if a newer version exists, false otherwise
81+
*/
82+
83+
function checkForNewerVersion(
84+
dirEntries: [string, vscode.FileType][],
85+
extensionId: string,
86+
currentVersion: string
87+
): boolean {
88+
const versionRegex = new RegExp(`^${extensionId}-(.+)$`)
89+
90+
return dirEntries
91+
.map(([name]) => name)
92+
.filter((name) => name.startsWith(extensionId))
93+
.some((name) => {
94+
const match = name.match(versionRegex)
95+
if (match && match[1]) {
96+
const version = semver.valid(semver.coerce(match[1]))
97+
return version !== null && semver.gt(version, currentVersion)
98+
}
99+
return false
100+
})
101+
}
102+
103+
/**
104+
* Handles the uninstall process by calling the callback and logging the event.
105+
*
106+
* @param extensionFullName - The full name of the extension including version
107+
* @param callback - The callback function to execute on uninstall
108+
*/
109+
async function handleUninstall(extensionFullName: string, callback: () => Promise<void>): Promise<void> {
110+
await callback()
111+
telemetry.aws_extensionUninstalled.run((span) => {
112+
span.record({})
113+
})
114+
getLogger().info(`UninstallExtension: ${extensionFullName} uninstalled successfully`)
115+
}
116+
63117
/**
64118
* Sets up a file system watcher to monitor the .obsolete file for changes and handle
65119
* extension un-installation if the extension is marked as obsolete.

0 commit comments

Comments
 (0)