Quasi un anno è passato dal lancio in anteprima del Nuovo Microsoft Teams e, anche se non proprio tutte le funzionalità sembrano propriamente stabili, Microsoft ha deciso già da tempo di distribuire automaticamente questa nuova versione al posto del caro, vecchio, consolidato, Teams Classic.
Ora, in linea di principio, questo aggiornamento automatico mi vede assolutamente favorevole, stando alle parole di Microsoft:

Gli utenti che hanno installato una versione diversa di Teams avranno la loro versione sostituita con la versione di cui è stato eseguito il provisioning

Ben venga l’aggiornameto automatico quindi? Ni!
L’aggiornamento al Nuovo Teams udite udite…non fa totale piazza pulita delle vecchie installazioni! (fino ad oggi almeno)

Come ben sappiamo, durante il suo ciclo di vita, l’installazione di Teams Classic ha avuto varie e fantasiose incarnazioni: spaziamo infatti dall’installazione su base utente in AppData o anche ProgramData fino al machine-wide installer e di certo vogliamo eliminare ogni residuo di queste vecchie installazioni!
A questo aggiungiamo che se siete abituati a lavorare con GPO e distribuzioni MSI questa volta dovrete farne a meno: New Teams non viene fornito come pacchetto MSI.

Tutto ciò premesso ho pensato quindi fosse utile scrivermi uno script PowerShell per avere maggiore controllo ed automazione sia sul processo di deploy del Nuovo Microsoft Teams, sia sulla rimozione di Teams Classic in tutte le sue varianti.

Disinstalliamo Teams Classic via PowerShell

Sappiamo che Teams Classic può essere stato installato sia a livello di profilo utente sia machine-wide, dobbiamo quindi tenerne conto per una rimozione completa.

Rimozione per utente

Controlliamo i registri utente e lanciamo la QuietUninstallString, questo ci permette di andare a colpo sicuro nel rimuovere Teams Classic senza l’utilizzo di path predefiniti.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# Specify the registry path
$reg = "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Teams"
# Get Teams uninstall information for each user
$userSids = Get-WmiObject Win32_UserProfile | Where-Object { $_.Special -eq $false } | Select-Object -ExpandProperty SID
foreach ($userSid in $userSids) {
    $userReg= "Registry::HKEY_USERS\$userSid\$reg"
    $teamsUninstallInfo = Get-ItemProperty -LiteralPath $userReg -ErrorAction SilentlyContinue
    # Display the Teams uninstall information for each user
    if ($teamsUninstallInfo) {
        $sid = New-Object System.Security.Principal.SecurityIdentifier($userSid)
        # Use Translate to find user from sid
        $objUser = $sid.Translate([System.Security.Principal.NTAccount])
        if ($teamsUninstallInfo.QuietUninstallString) {
            Start-Process -FilePath "cmd" -ArgumentList "/c", $teamsUninstallInfo.QuietUninstallString -Wait
        }
        # Cleanup registry
        if (Test-Path -path $userReg) {
            Remove-Item $userReg -Recurse -Force
        }
    }
}

Rimozione machine-wide

Utilizziamo i guid conosciuti di Teams e anche qui andiamo a sfruttare il registry per disinstallare tramite msiexec.
Rispetto a molte altre fonti trovate in rete questo metodo è molto più veloce perchè evita l’utilizzo di Get-WmiObject -Class Win32_Product per ottenere la lista dei programmi installati.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# Known Guid
$msiPkg32Guid = "{39AF0813-FA7B-4860-ADBE-93B9B214B914}"
$msiPkg64Guid = "{731F6BAA-A986-45A4-8936-7C3AAAAA760B}"
$uninstallReg64 = Get-Item -Path HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* -ErrorAction SilentlyContinue | Get-ItemProperty | Where-Object { $_.DisplayName -match 'Teams Machine-Wide Installer' }
$uninstallReg32 = Get-Item -Path HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\* -ErrorAction SilentlyContinue | Get-ItemProperty | Where-Object { $_.DisplayName -match 'Teams Machine-Wide Installer' }
if ($uninstallReg64) {
    $msiExecUninstallArgs = "/X $msiPkg64Guid /quiet"
} elseif ($uninstallReg32) {
    $msiExecUninstallArgs = "/X $msiPkg32Guid /quiet"
} else {
    return
}
$p = Start-Process "msiexec.exe" -ArgumentList $msiExecUninstallArgs -Wait -PassThru -WindowStyle Hidden

Installiamo New Teams via PowerShell

Partiamo col dire che i metodi uffialmente supportati per l’installazione li troviamo qui https://learn.microsoft.com/en-us/microsoftteams/new-teams-bulk-install-client e scopriamo che dovremo necessariamente utilizzare teamsbootstrapper.exe per il nostro deploy, quindi per prima cosa scarichiamolo tramite una helper function.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function DownloadFile {
    param(
        [Parameter(Mandatory=$true)]
        [string]$url,
        [Parameter(Mandatory=$true)]
        [string]$fileName,
        [string]$path = [System.Environment]::GetEnvironmentVariable('TEMP','Machine')
    )
    # Construct WebClient object
    $webClient = New-Object -TypeName System.Net.WebClient
    $file = $null
    # Create path if it doesn't exist
    if (-not(Test-Path -Path $path)) {
        New-Item -Path $path -ItemType Directory -Force | Out-Null
    }
    # Download
    try {
        $outputPath = Join-Path -Path $path -ChildPath $fileName
        $webClient.DownloadFile($url, $outputPath)
        $file = $outputPath
    }
    catch {}
    # Dispose of the WebClient object
    $webClient.Dispose()
    return $file
}
$BootstrapperPath = DownloadFile "https://go.microsoft.com/fwlink/?linkid=2243204&clcid=0x409" "bootstrapper.exe"

Poi passiamo all’installazione tramite bootstrapper.exe, possiamo lasciare che scarichi Teams direttamente da internet o fornire un path locale o UNC se preferiamo risparmiare tempo e banda

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function InstallTeams {
    param(
        [Parameter(Mandatory=$true)]
        [string]$bootstrapperPath,
        [string]$teamsPackagePath = ''
    )
    try {
        # Using the teamsbootstrapper.exe -p command always guarantees the latest Teams client is installed.
        # Use -o with path to Teams's MSIX package minimizing the amount of bandwidth used for the initial installation.
        # The MSIX can exist in a local path or UNC.
        if ($teamsPackagePath -ne '') {
            $arg = "-o $teamsPackagePath"
        }
        $r = & $bootstrapperPath -p $arg
        $resultObj = try { $r | ConvertFrom-Json } catch { $null }
        if ($resultObj -eq $null -or $resultObj.success -eq $false) {
            throw ''
        }
        return $true
    }
    catch {
        return $false
    }
}
InstallTeams -BootstrapperPath $BootstrapperPath

Teams-Posh, lo script definitivo per rimozione e installazione di Teams

Non ci resta che mettere insieme tutti i pezzi visti fino ad ora e buttare giù lo script completo e con il necessario logging.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
<#
.SYNOPSIS
Teams-Posh installs or uninstalls Microsoft Teams.

.DESCRIPTION
This script allows for the installation or uninstallation of Microsoft Teams. 
When installing, it downloads the bootstrapper and Teams package if not provided, 
and then installs Microsoft Teams.
When uninstalling, it removes the installed Microsoft Teams application, 
this includes Teams Classic uninstallation querying related registry keys thus 
avoiding use of very slow call to "Get-WmiObject -Class Win32_Product".

.PARAMETER Action
Specifies the action to perform. Valid values are 'Install' or 'Uninstall'.

.PARAMETER BootstrapperPath
Specifies the path to the bootstrapper executable. 
If not provided, it will be downloaded by Microsoft website.

.PARAMETER TeamsPackagePath
Specifies the path to the Microsoft Teams package. 
If not provided (required for installation), it will be downloaded.

.EXAMPLE
.\Teams-Posh.ps1 -Action Install
Installs Microsoft Teams.

.EXAMPLE
.\Teams-Posh.ps1 -Action Uninstall
Uninstalls Microsoft Teams.

.NOTES
Author:[lestoilfante](https://github.com/lestoilfante)
#>


param (
    [Parameter(Mandatory=$true)]
    [ValidateSet('Install', 'Uninstall')]
    [string]$Action,
    [string]$BootstrapperPath = '',
    [string]$TeamsPackagePath = ''
)

function Teams-Posh {
    # Check running with elevated privileges
    if (!([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {
        Log "This script requires elevation. Please run as administrator"
        exit 1
    }

    if ($BootstrapperPath -eq '') {
        $BootstrapperPath = DownloadFile "https://go.microsoft.com/fwlink/?linkid=2243204&clcid=0x409" "bootstrapper.exe"
        if ($BootstrapperPath -eq $null) { exit 1 }
    }

    if ($Action -eq 'Install') {
        $install = InstallTeams -BootstrapperPath $BootstrapperPath -TeamsPackagePath $TeamsPackagePath
        if ($install -eq $true) { exit 0 }
        exit 1
    }

    if ($Action -eq 'Uninstall') {
        RemoveTeamsClassicWide
        RemoveTeamsClassic
        RemoveTeams $BootstrapperPath
        exit 0
    }
}


function InstallTeams {
    param(
        [Parameter(Mandatory=$true)]
        [string]$bootstrapperPath,
        [string]$teamsPackagePath = ''
    )
    try {
        # Using the teamsbootstrapper.exe -p command always guarantees the latest Teams client is installed.
        # Use -o with path to Teams's MSIX package minimizing the amount of bandwidth used for the initial installation.
        # The MSIX can exist in a local path or UNC.
        if ($teamsPackagePath -ne '') {
            $arg = "-o $teamsPackagePath"
        } else { Log 'Downloading Teams' }
        $r = & $bootstrapperPath -p $arg
        $resultObj = try { $r | ConvertFrom-Json } catch { $null }
        if ($resultObj -eq $null -or $resultObj.success -eq $false) {
            throw ''
        }
        Log 'Teams installation done'
        return $true
    }
    catch {
        Log 'ERROR: Teams installation failed'
        return $false
    }
}

function RemoveTeamsClassicWide {
    # Known Guid
    $msiPkg32Guid = "{39AF0813-FA7B-4860-ADBE-93B9B214B914}"
    $msiPkg64Guid = "{731F6BAA-A986-45A4-8936-7C3AAAAA760B}"
    $uninstallReg64 = Get-Item -Path HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* -ErrorAction SilentlyContinue | Get-ItemProperty | Where-Object { $_.DisplayName -match 'Teams Machine-Wide Installer' }
    $uninstallReg32 = Get-Item -Path HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\* -ErrorAction SilentlyContinue | Get-ItemProperty | Where-Object { $_.DisplayName -match 'Teams Machine-Wide Installer' }
    if ($uninstallReg64) {
        $msiExecUninstallArgs = "/X $msiPkg64Guid /quiet"
        Log "Teams Classic Machine-Wide Installer x64 found."
    } elseif ($uninstallReg32) {
        $msiExecUninstallArgs = "/X $msiPkg32Guid /quiet"
        Log "Teams Machine-Wide Installer x86 found."
    } else {
        return
    }
    $p = Start-Process "msiexec.exe" -ArgumentList $msiExecUninstallArgs -Wait -PassThru -WindowStyle Hidden
    if ($p.ExitCode -eq 0) {
        Log "Teams Classic Machine-Wide uninstalled."
    } else {
        Log "ERROR: Teams Classic Machine-Wide uninstall failed with exit code $($p.ExitCode)"
    }
}

function RemoveTeamsClassic {
    # Specify the registry path
    $reg = "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Teams"
    # Get Teams uninstall information for each user
    $userSids = Get-WmiObject Win32_UserProfile | Where-Object { $_.Special -eq $false } | Select-Object -ExpandProperty SID
    foreach ($userSid in $userSids) {
        $userReg= "Registry::HKEY_USERS\$userSid\$reg"
        $teamsUninstallInfo = Get-ItemProperty -LiteralPath $userReg -ErrorAction SilentlyContinue
        # Display the Teams uninstall information for each user
        if ($teamsUninstallInfo) {
            $sid = New-Object System.Security.Principal.SecurityIdentifier($userSid)
            # Use Translate to find user from sid
            $objUser = $sid.Translate([System.Security.Principal.NTAccount])
            if ($teamsUninstallInfo.QuietUninstallString) {
                Start-Process -FilePath "cmd" -ArgumentList "/c", $teamsUninstallInfo.QuietUninstallString -Wait
                Log "Teams Classic Removed for user $($objUser.Value)"
            }
            # Cleanup registry
            if (Test-Path -path $userReg) {
                Remove-Item $userReg -Recurse -Force
            }
        }
    }
}

function RemoveTeams {
    param(
        [string]$bootstrapper = ''
    )
    try{
        $appx = Get-AppxPackage -AllUsers | Where-Object { $PSItem.Name -eq "MSTeams" }
        if ($appx) {
            Log "Teams $($appx.Version) package found"
            $appx | Remove-AppxPackage -AllUsers
        } else { Log "No Teams package found" }
        if($bootstrapper -ne '') {
            Log "Deprovisioning Teams using $bootstrapper"
            $r = & $bootstrapper -x
            $resultObj = try { $r | ConvertFrom-Json } catch { $null }
            if ($resultObj -eq $null) {
                throw ''
            }
            Log "Deprovisioning Teams using $bootstrapper done"
        }
    }
    catch {
        Log "ERROR: Teams package remove error"
    }
}

function DownloadFile {
    param(
        [Parameter(Mandatory=$true)]
        [string]$url,
        [Parameter(Mandatory=$true)]
        [string]$fileName,
        [string]$path = [System.Environment]::GetEnvironmentVariable('TEMP','Machine')
    )
    # Construct WebClient object
    $webClient = New-Object -TypeName System.Net.WebClient
    $file = $null
    # Create path if it doesn't exist
    if (-not(Test-Path -Path $path)) {
        New-Item -Path $path -ItemType Directory -Force | Out-Null
    }
    # Download
    try {
        Log "Download of $fileName start"
        $outputPath = Join-Path -Path $path -ChildPath $fileName
        $webClient.DownloadFile($url, $outputPath)
        Log "Download of $fileName done"
        $file = $outputPath
    }
    catch {
        Log "ERROR: Download of $fileName failed"
    }
    # Dispose of the WebClient object
    $webClient.Dispose()
    return $file
}

function Log {
	param (
		[string]$Text
	)
	$timestamp = "{0:yyyy-MM-dd HH:mm:ss}" -f [DateTime]::Now
	Write-Information -MessageData "$timestamp `- $($Text)" -InformationAction Continue
}

Teams-Posh