Skip to content

Commit b6c40e1

Browse files
authored
fix(amazonq): do not trigger uninstall when extension auto-updates (#5673)
Do not trigger uninstall when extension auto-updates License: I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent 1205101 commit b6c40e1

File tree

3 files changed

+83
-36
lines changed

3 files changed

+83
-36
lines changed

packages/amazonq/src/extension.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ export async function activateAmazonQCommon(context: vscode.ExtensionContext, is
131131
registerCommands(context)
132132

133133
// Handle Amazon Q Extension un-installation.
134-
setupUninstallHandler(VSCODE_EXTENSION_ID.amazonq, context)
134+
setupUninstallHandler(VSCODE_EXTENSION_ID.amazonq, context.extension.packageJSON.version, context)
135135

136136
// Hide the Amazon Q tree in toolkit explorer
137137
await setContext('aws.toolkit.amazonq.dismissed', true)

packages/core/src/extension.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ export async function activateCommon(
155155
)
156156

157157
// Handle AWS Toolkit un-installation.
158-
setupUninstallHandler(VSCODE_EXTENSION_ID.awstoolkit, context)
158+
setupUninstallHandler(VSCODE_EXTENSION_ID.awstoolkit, context.extension.id, context)
159159

160160
// auth
161161
await initializeAuth(globals.loginManager)

packages/core/src/shared/handleUninstall.ts

Lines changed: 81 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -7,59 +7,105 @@
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'
14-
import { extensionVersion } from './vscode/env'
1515

1616
/**
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.
17+
* Checks if an extension has been uninstalled and performs a callback if so.
18+
* This function differentiates between an uninstall and an auto-update.
1919
*
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}
20+
* @param extensionId - The ID of the extension to check (e.g., VSCODE_EXTENSION_ID.awstoolkit)
21+
* @param extensionsPath - The file system path to the VS Code extensions directory
22+
* @param obsoletePath - The file system path to the .obsolete file
23+
* @param onUninstallCallback - A callback function to execute if the extension is uninstalled
2524
*/
2625
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>
26+
extensionId: typeof VSCODE_EXTENSION_ID.awstoolkit | typeof VSCODE_EXTENSION_ID.amazonq,
27+
extensionVersion: string,
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(extensionDirEntries, extensionId, extensionVersion)
52+
53+
if (!newerVersionExists) {
54+
// No newer version exists, so this is likely an uninstall
55+
await handleUninstall(extensionFullName, onUninstallCallback)
56+
} else {
57+
getLogger().info(`UpdateExtension: ${extensionFullName} is being updated - not an uninstall`)
58+
}
5759
}
5860
} catch (error) {
5961
getLogger().error(`UninstallExtension: Failed to check .obsolete: ${error}`)
6062
}
6163
}
6264

65+
/**
66+
* Checks if a newer version of the extension exists in the extensions directory.
67+
* The isExtensionInstalled fn is used to determine if the extension is installed using the vscode API
68+
* whereas this function checks for the newer version in the extension directory for scenarios where
69+
* the old extension is un-installed and the new extension in downloaded but not installed.
70+
*
71+
* @param dirEntries - The entries in the extensions directory
72+
* @param extensionId - The ID of the extension to check
73+
* @param currentVersion - The current version of the extension
74+
* @returns True if a newer version exists, false otherwise
75+
*/
76+
77+
function checkForNewerVersion(
78+
dirEntries: [string, vscode.FileType][],
79+
extensionId: string,
80+
currentVersion: string
81+
): boolean {
82+
const versionRegex = new RegExp(`^${extensionId}-(.+)$`)
83+
84+
return dirEntries
85+
.map(([name]) => name)
86+
.filter((name) => name.startsWith(extensionId))
87+
.some((name) => {
88+
const match = name.match(versionRegex)
89+
if (match && match[1]) {
90+
const version = semver.valid(semver.coerce(match[1]))
91+
return version !== null && semver.gt(version, currentVersion)
92+
}
93+
return false
94+
})
95+
}
96+
97+
/**
98+
* Handles the uninstall process by calling the callback and logging the event.
99+
*
100+
* @param extensionFullName - The full name of the extension including version
101+
* @param callback - The callback function to execute on uninstall
102+
*/
103+
async function handleUninstall(extensionFullName: string, callback: () => Promise<void>): Promise<void> {
104+
await callback()
105+
telemetry.aws_extensionUninstalled.run(() => {})
106+
getLogger().info(`UninstallExtension: ${extensionFullName} uninstalled successfully`)
107+
}
108+
63109
/**
64110
* Sets up a file system watcher to monitor the .obsolete file for changes and handle
65111
* extension un-installation if the extension is marked as obsolete.
@@ -71,6 +117,7 @@ async function checkExtensionUninstall(
71117
*/
72118
export function setupUninstallHandler(
73119
extensionName: typeof VSCODE_EXTENSION_ID.awstoolkit | typeof VSCODE_EXTENSION_ID.amazonq,
120+
extensionVersion: string,
74121
context: vscode.ExtensionContext,
75122
callback: () => Promise<void> = async () => {}
76123
): void {
@@ -86,7 +133,7 @@ export function setupUninstallHandler(
86133
const fileWatcher = vscode.workspace.createFileSystemWatcher(watchPattern)
87134

88135
const checkUninstallHandler = () =>
89-
checkExtensionUninstall(extensionName, extensionsDirPath, obsoleteFilePath, callback)
136+
checkExtensionUninstall(extensionName, extensionVersion, extensionsDirPath, obsoleteFilePath, callback)
90137
fileWatcher.onDidCreate(checkUninstallHandler)
91138
fileWatcher.onDidChange(checkUninstallHandler)
92139

0 commit comments

Comments
 (0)