From 37fe144c4b6c7150d1454ab97469a40249509e65 Mon Sep 17 00:00:00 2001 From: Mathijs Verbeeck Date: Fri, 20 Sep 2024 11:16:11 +0200 Subject: [PATCH] Updates 'teams cache remove' command to support the new client. Closes #5998 --- docs/docs/cmd/teams/cache/cache-remove.mdx | 11 +- docs/docs/v10-upgrade-guidance.mdx | 12 ++ .../teams/commands/cache/cache-remove.spec.ts | 150 ++++++++++++++++-- src/m365/teams/commands/cache/cache-remove.ts | 113 +++++++++---- 4 files changed, 240 insertions(+), 46 deletions(-) diff --git a/docs/docs/cmd/teams/cache/cache-remove.mdx b/docs/docs/cmd/teams/cache/cache-remove.mdx index e9d5f0a39fb..b37fb31e224 100644 --- a/docs/docs/cmd/teams/cache/cache-remove.mdx +++ b/docs/docs/cmd/teams/cache/cache-remove.mdx @@ -13,6 +13,9 @@ m365 teams cache remove [options] ## Options ```md definition-list +`-c, --client` +: Teams client to target. Possible values are: `new` or `classic`. Default value is `new`. + `-f, --force` : Don't prompt for confirmation ``` @@ -43,12 +46,18 @@ The command works only on Windows and macOS. If you run it on a different operat ## Examples -Removes the Microsoft Teams client cache +Removes the Microsoft Teams client cache for the new client. ```sh m365 teams cache remove ``` +Removes the Microsoft Teams client cache for the classic client. + +```sh +m365 teams cache remove --client classic +``` + ## Response The command won't return a response on success. diff --git a/docs/docs/v10-upgrade-guidance.mdx b/docs/docs/v10-upgrade-guidance.mdx index 49bf8f02419..a4e11b59118 100644 --- a/docs/docs/v10-upgrade-guidance.mdx +++ b/docs/docs/v10-upgrade-guidance.mdx @@ -271,6 +271,18 @@ Please update your scripts not to use the `overwrite` option. ## Teams +### Enhanced `teams cache remove` command + +We enhanced the [teams cache remove](./cmd/teams/cache/cache-remove.mdx) command to support the removal of the cache from the new Teams client. We also made it so that by default, the command will remove the cache for the `new` client. + +To still support the old client, we have added the option `--client`, in which you can specify `classic` as value to remove the cache of the old Teams client. + +#### What action do I need to take? + +Add the `--client` option to your scripts to specify which client's cache you want to remove. Otherwise, upon the release of v10, it will attempt to remove the cache from the new client. + +## Teams + ### Removes duplicate property from 'teams tab list' command. For the [teams tab list](./cmd/teams/tab/tab-list.mdx) command we removed the `teamsAppTabId` from the command output as it was a duplicate of the `teamsApp/id` property. diff --git a/src/m365/teams/commands/cache/cache-remove.spec.ts b/src/m365/teams/commands/cache/cache-remove.spec.ts index 84fa5c0ec85..bb4726db0af 100644 --- a/src/m365/teams/commands/cache/cache-remove.spec.ts +++ b/src/m365/teams/commands/cache/cache-remove.spec.ts @@ -12,6 +12,7 @@ import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './cache-remove.js'; +import os, { homedir } from 'os'; describe(commands.CACHE_REMOVE, () => { const processOutput = `ProcessId @@ -20,6 +21,7 @@ describe(commands.CACHE_REMOVE, () => { 11352`; let log: string[]; let logger: Logger; + let loggerLogSpy: sinon.SinonSpy; let commandInfo: CommandInfo; before(() => { @@ -45,6 +47,7 @@ describe(commands.CACHE_REMOVE, () => { log.push(msg); } }; + loggerLogSpy = sinon.spy(logger, 'log'); sinon.stub(cli, 'promptForConfirmation').resolves(true); }); @@ -84,6 +87,16 @@ describe(commands.CACHE_REMOVE, () => { assert(confirmationStub.calledOnce); }); + it('fails validation if client is not a valid client option', async () => { + sinon.stub(process, 'platform').value('win32'); + const actual = await command.validate({ + options: { + client: 'invalid' + } + }, commandInfo); + assert.notStrictEqual(actual, true); + }); + it('fails validation if called from docker container.', async () => { sinon.stub(process, 'platform').value('win32'); sinon.stub(process, 'env').value({ 'CLIMICROSOFT365_ENV': 'docker' }); @@ -114,7 +127,7 @@ describe(commands.CACHE_REMOVE, () => { assert.strictEqual(actual, true); }); - it('fails to remove teams cache when exec fails randomly when killing teams.exe process', async () => { + it('fails to remove teams cache when exec fails randomly when killing teams.exe process using classic client', async () => { sinon.stub(process, 'platform').value('win32'); sinon.stub(process, 'env').value({ 'CLIMICROSOFT365_ENV': '' }); sinon.stub(fs, 'existsSync').returns(true); @@ -125,10 +138,10 @@ describe(commands.CACHE_REMOVE, () => { } throw 'Invalid request'; }); - await assert.rejects(command.action(logger, { options: { force: true } } as any), new CommandError('random error')); + await assert.rejects(command.action(logger, { options: { client: 'classic', force: true } } as any), new CommandError('random error')); }); - it('fails to remove teams cache when exec fails randomly when removing cache folder', async () => { + it('fails to remove teams cache when exec fails randomly when removing cache folder using classic client', async () => { sinon.stub(process, 'platform').value('win32'); sinon.stub(process, 'env').value({ 'CLIMICROSOFT365_ENV': '', APPDATA: 'C:\\Users\\Administrator\\AppData\\Roaming' }); sinon.stub(process, 'kill' as any).returns(null); @@ -143,10 +156,41 @@ describe(commands.CACHE_REMOVE, () => { } throw 'Invalid request'; }); - await assert.rejects(command.action(logger, { options: { force: true } } as any), new CommandError('random error')); + await assert.rejects(command.action(logger, { options: { client: 'classic', force: true } } as any), new CommandError('random error')); + }); + + it('shows error message when exec fails when removing the teams cache folder on mac os', async () => { + const deleteError = { + code: 1, + killed: false, + signal: null, + cmd: 'rm -r "/Users/John/Library/Group Containers/UBF8T346G9.com.microsoft.teams"', + stdout: '', + stderr: 'rm: /Users/John/Library/Group Containers/UBF8T346G9.com.microsoft.teams: Operation not permitted\\n' + }; + + sinon.stub(process, 'platform').value('darwin'); + sinon.stub(process, 'env').value({ 'CLIMICROSOFT365_ENV': '' }); + sinon.stub(process, 'kill' as any).returns(null); + sinon.stub(fs, 'existsSync').returns(true); + + sinon.stub(command, 'exec' as any).callsFake(async (opts) => { + if (opts === `ps ax | grep MacOS/MSTeams -m 1 | grep -v grep | awk '{ print $1 }'`) { + return {}; + } + if (opts === `rm -r "${homedir}/Library/Group Containers/UBF8T346G9.com.microsoft.teams"`) { + return; + } + if (opts === `rm -r "${homedir}/Library/Containers/com.microsoft.teams2"`) { + throw deleteError; + } + throw 'Invalid request'; + }); + await command.action(logger, { options: { force: true } } as any); + assert(loggerLogSpy.calledWith('Deleting the folder failed. Please have a look at the following URL to delete the folders manually: https://answers.microsoft.com/en-us/msteams/forum/all/clearing-cache-on-microsoft-teams/35876f6b-eb1a-4b77-bed1-02ce3277091f')); }); - it('removes Teams cache from macOs platform without prompting.', async () => { + it('removes Teams cache from macOs platform without prompting using classic client', async () => { sinon.stub(process, 'platform').value('darwin'); sinon.stub(process, 'env').value({ 'CLIMICROSOFT365_ENV': '' }); sinon.stub(command, 'exec' as any).returns({ stdout: '' }); @@ -156,13 +200,14 @@ describe(commands.CACHE_REMOVE, () => { await command.action(logger, { options: { force: true, - verbose: true + verbose: true, + client: 'classic' } }); assert(true); }); - it('removes teams cache when teams is currently not active', async () => { + it('removes teams cache when teams is currently not active using the classic client', async () => { sinon.stub(process, 'platform').value('win32'); sinon.stub(process, 'env').value({ 'CLIMICROSOFT365_ENV': '', APPDATA: 'C:\\Users\\Administrator\\AppData\\Roaming' }); sinon.stub(process, 'kill' as any).returns(null); @@ -180,13 +225,14 @@ describe(commands.CACHE_REMOVE, () => { await command.action(logger, { options: { force: true, - verbose: true + verbose: true, + client: 'classic' } }); assert(true); }); - it('removes Teams cache from win32 platform without prompting.', async () => { + it('removes Teams cache from win32 platform without prompting using the classic client', async () => { sinon.stub(process, 'platform').value('win32'); sinon.stub(process, 'env').value({ 'CLIMICROSOFT365_ENV': '', APPDATA: 'C:\\Users\\Administrator\\AppData\\Roaming' }); sinon.stub(process, 'kill' as any).returns(null); @@ -200,6 +246,30 @@ describe(commands.CACHE_REMOVE, () => { throw 'Invalid request'; }); sinon.stub(fs, 'existsSync').returns(true); + await command.action(logger, { + options: { + force: true, + verbose: true, + client: 'classic' + } + }); + assert(true); + }); + + it('removes Teams cache from win32 platform without prompting using the new client', async () => { + sinon.stub(process, 'platform').value('win32'); + sinon.stub(process, 'env').value({ 'CLIMICROSOFT365_ENV': '', APPDATA: 'C:\\Users\\Administrator\\AppData\\Roaming', LOCALAPPDATA: 'C:\\Users\\Administrator\\AppData\\Local' }); + sinon.stub(process, 'kill' as any).returns(null); + sinon.stub(command, 'exec' as any).callsFake(async (opts) => { + if (opts === 'wmic process where caption="ms-teams.exe" get ProcessId') { + return { stdout: processOutput }; + } + if (opts === 'rmdir /s /q "C:\\Users\\Administrator\\AppData\\Local\\Packages\\MSTeams_8wekyb3d8bbwe\\LocalCache\\Microsoft\\MSTeams"') { + return; + } + throw 'Invalid request'; + }); + sinon.stub(fs, 'existsSync').returns(true); await command.action(logger, { options: { force: true, @@ -209,7 +279,7 @@ describe(commands.CACHE_REMOVE, () => { assert(true); }); - it('removes Teams cache from darwin platform with prompting.', async () => { + it('removes Teams cache from darwin platform with prompting using the classic client', async () => { sinon.stub(process, 'platform').value('darwin'); sinon.stub(process, 'env').value({ 'CLIMICROSOFT365_ENV': '' }); sinon.stub(command, 'exec' as any).returns({ stdout: 'pid' }); @@ -218,12 +288,70 @@ describe(commands.CACHE_REMOVE, () => { await command.action(logger, { options: { - debug: true + debug: true, + client: 'classic' } }); assert(true); }); + it('removes Teams cache from darwin platform with prompting', async () => { + sinon.stub(process, 'platform').value('darwin'); + sinon.stub(process, 'env').value({ 'CLIMICROSOFT365_ENV': '' }); + sinon.stub(command, 'exec' as any).returns({ stdout: '1111' }); + sinon.stub(process, 'kill' as any).returns(null); + sinon.stub(fs, 'existsSync').returns(true); + + await command.action(logger, { + options: { + debug: true, + client: 'new' + } + }); + assert(true); + }); + + it('removes teams cache when teams is currently not running on macOS', async () => { + sinon.stub(process, 'platform').value('darwin'); + sinon.stub(process, 'env').value({ 'CLIMICROSOFT365_ENV': '' }); + sinon.stub(process, 'kill' as any).returns(null); + sinon.stub(command, 'exec' as any).callsFake(async (opts) => { + if (opts === `ps ax | grep MacOS/MSTeams -m 1 | grep -v grep | awk '{ print $1 }'`) { + return {}; + } + if (opts === `rm -r "${os.homedir()}/Library/Group Containers/UBF8T346G9.com.microsoft.teams"`) { + return; + } + if (opts === `rm -r "${os.homedir()}/Library/Containers/com.microsoft.teams2"`) { + return; + } + throw 'Invalid request'; + }); + sinon.stub(fs, 'existsSync').returns(true); + + await command.action(logger, { + options: { + force: true, + verbose: true, + client: 'new' + } + }); + assert(true); + }); + + + it('aborts cache clearing when no cache folder is found using the classic client', async () => { + sinon.stub(process, 'platform').value('darwin'); + sinon.stub(process, 'env').value({ 'CLIMICROSOFT365_ENV': '' }); + sinon.stub(fs, 'existsSync').returns(false); + await command.action(logger, { + options: { + verbose: true, + client: 'classic' + } + }); + }); + it('aborts cache clearing when no cache folder is found', async () => { sinon.stub(process, 'platform').value('darwin'); sinon.stub(process, 'env').value({ 'CLIMICROSOFT365_ENV': '' }); diff --git a/src/m365/teams/commands/cache/cache-remove.ts b/src/m365/teams/commands/cache/cache-remove.ts index 8b8db97eb23..d96a002edc1 100644 --- a/src/m365/teams/commands/cache/cache-remove.ts +++ b/src/m365/teams/commands/cache/cache-remove.ts @@ -14,6 +14,7 @@ interface CommandArgs { } interface Options extends GlobalOptions { + client?: string; force?: boolean; } @@ -22,6 +23,8 @@ interface Win32Process { } class TeamsCacheRemoveCommand extends AnonymousCommand { + private static readonly allowedClients: string[] = ['new', 'classic']; + public get name(): string { return commands.CACHE_REMOVE; } @@ -41,6 +44,7 @@ class TeamsCacheRemoveCommand extends AnonymousCommand { #initTelemetry(): void { this.telemetry.push((args: CommandArgs) => { Object.assign(this.telemetryProperties, { + client: args.options.client, force: !!args.options.force }); }); @@ -48,6 +52,10 @@ class TeamsCacheRemoveCommand extends AnonymousCommand { #initOptions(): void { this.options.unshift( + { + option: '-c, --client', + autocomplete: TeamsCacheRemoveCommand.allowedClients + }, { option: '-f, --force' } @@ -56,7 +64,11 @@ class TeamsCacheRemoveCommand extends AnonymousCommand { #initValidators(): void { this.validators.push( - async () => { + async (args: CommandArgs) => { + if (args.options.client && !TeamsCacheRemoveCommand.allowedClients.includes(args.options.client.toLowerCase())) { + return `'${args.options.client}' is not a valid value for option 'client'. Allowed values are ${TeamsCacheRemoveCommand.allowedClients.join(', ')}`; + } + if (process.env.CLIMICROSOFT365_ENV === 'docker') { return 'Because you\'re running CLI for Microsoft 365 in a Docker container, we can\'t clear the cache on your host. Instead run this command on your host using "npx ..."'; } @@ -73,7 +85,7 @@ class TeamsCacheRemoveCommand extends AnonymousCommand { public async commandAction(logger: Logger, args: CommandArgs): Promise { try { if (args.options.force) { - await this.clearTeamsCache(logger); + await this.clearTeamsCache(args.options.client?.toLowerCase() || 'new', logger); } else { await logger.logToStderr('This command will execute the following steps.'); @@ -83,7 +95,7 @@ class TeamsCacheRemoveCommand extends AnonymousCommand { const result = await cli.promptForConfirmation({ message: `Are you sure you want to clear your Microsoft Teams cache?` }); if (result) { - await this.clearTeamsCache(logger); + await this.clearTeamsCache(args.options.client?.toLowerCase() || 'new', logger); } } } @@ -92,13 +104,20 @@ class TeamsCacheRemoveCommand extends AnonymousCommand { } } - private async clearTeamsCache(logger: Logger): Promise { - const filePath = await this.getTeamsCacheFolderPath(logger); - const folderExists = await this.checkIfCacheFolderExists(filePath, logger); + private async clearTeamsCache(client: string, logger: Logger): Promise { + const filePaths = await this.getTeamsCacheFolderPaths(client, logger); + + let folderExists = true; + for (const filePath of filePaths) { + const exists = await this.checkIfCacheFolderExists(filePath, logger); + if (!exists) { + folderExists = false; + } + } if (folderExists) { - await this.killRunningProcess(logger); - await this.removeCacheFiles(filePath, logger); + await this.killRunningProcess(client, logger); + await this.removeCacheFiles(filePaths, logger); await logger.logToStderr('Teams cache cleared!'); } else { @@ -107,24 +126,34 @@ class TeamsCacheRemoveCommand extends AnonymousCommand { } - private async getTeamsCacheFolderPath(logger: Logger): Promise { + private async getTeamsCacheFolderPaths(client: string, logger: Logger): Promise { const platform = process.platform; if (this.verbose) { await logger.logToStderr(`Getting path of Teams cache folder for platform ${platform}...`); } - - let filePath = ''; + const filePaths: string[] = []; switch (platform) { case 'win32': - filePath = `${process.env.APPDATA}\\Microsoft\\Teams`; + if (client === 'classic') { + filePaths.push(`${process.env.APPDATA}\\Microsoft\\Teams`); + } + else { + filePaths.push(`${process.env.LOCALAPPDATA}\\Packages\\MSTeams_8wekyb3d8bbwe\\LocalCache\\Microsoft\\MSTeams`); + } break; case 'darwin': - filePath = `${homedir}/Library/Application Support/Microsoft/Teams`; + if (client === 'classic') { + filePaths.push(`${homedir}/Library/Application Support/Microsoft/Teams`); + } + else { + filePaths.push(`${homedir}/Library/Group Containers/UBF8T346G9.com.microsoft.teams`); + filePaths.push(`${homedir}/Library/Containers/com.microsoft.teams2`); + } break; } - return filePath; + return filePaths; } private async checkIfCacheFolderExists(filePath: string, logger: Logger): Promise { @@ -135,7 +164,7 @@ class TeamsCacheRemoveCommand extends AnonymousCommand { return fs.existsSync(filePath); } - private async killRunningProcess(logger: Logger): Promise { + private async killRunningProcess(client: string, logger: Logger): Promise { if (this.verbose) { await logger.logToStderr('Stopping Teams client...'); } @@ -145,10 +174,21 @@ class TeamsCacheRemoveCommand extends AnonymousCommand { switch (platform) { case 'win32': - cmd = 'wmic process where caption="Teams.exe" get ProcessId'; + if (client === 'classic') { + cmd = 'wmic process where caption="Teams.exe" get ProcessId'; + } + else { + cmd = 'wmic process where caption="ms-teams.exe" get ProcessId'; + } break; case 'darwin': - cmd = `ps ax | grep MacOS/Teams -m 1 | grep -v grep | awk '{ print $1 }'`; + if (client === 'classic') { + cmd = `ps ax | grep MacOS/Teams -m 1 | grep -v grep | awk '{ print $1 }'`; + } + else { + cmd = `ps ax | grep MacOS/MSTeams -m 1 | grep -v grep | awk '{ print $1 }'`; + } + break; } @@ -158,42 +198,47 @@ class TeamsCacheRemoveCommand extends AnonymousCommand { const cmdOutput = await this.exec(cmd); - if (platform === 'darwin') { + if (platform === 'darwin' && cmdOutput.stdout) { process.kill(parseInt(cmdOutput.stdout)); } else if (platform === 'win32') { const processJson: Win32Process[] = formatting.parseCsvToJson(cmdOutput.stdout); - processJson.filter(proc => proc.ProcessId).map((proc: Win32Process) => { + for (const proc of processJson) { process.kill(proc.ProcessId); - }); + } } if (this.verbose) { await logger.logToStderr('Teams client closed'); } } - private async removeCacheFiles(filePath: string, logger: Logger): Promise { + private async removeCacheFiles(filePaths: string[], logger: Logger): Promise { if (this.verbose) { await logger.logToStderr('Removing Teams cache files...'); } const platform = process.platform; - let cmd = ''; + const baseCmd = platform === 'win32' ? 'rmdir /s /q ' : 'rm -r '; - switch (platform) { - case 'win32': - cmd = `rmdir /s /q "${filePath}"`; - break; - case 'darwin': - cmd = `rm -r "${filePath}"`; - break; - } + for (const filePath of filePaths) { + const cmd = `${baseCmd}"${filePath}"`; - if (this.debug) { - await logger.logToStderr(cmd); - } + if (this.debug) { + await logger.logToStderr(cmd); + } - await this.exec(cmd); + try { + await this.exec(cmd); + } + catch (err: any) { + if (err?.stderr?.includes('Operation not permitted')) { + await logger.log('Deleting the folder failed. Please have a look at the following URL to delete the folders manually: https://answers.microsoft.com/en-us/msteams/forum/all/clearing-cache-on-microsoft-teams/35876f6b-eb1a-4b77-bed1-02ce3277091f'); + } + else { + throw err; + } + } + } } private exec = util.promisify(child_process.exec);