6509d27253 | ||
---|---|---|
.. | ||
Screenshots | ||
Create_VHDx_from_msix.ps1 | ||
LabConfig.ps1 | ||
readme.md |
readme.md
AppAttach
About the lab
In following lab you learn how AppAttach works. The environment is similar to Windows Virtual Desktop, where you have multiple Windows 10 session hosts. App attach will be demonstrated on PowerShell 7 msix app that will be downloaded from PowerShell repository.
#sample labconfig with enabled telemetry (Full)
$LabConfig=@{ DomainAdminName='LabAdmin'; AdminPassword='LS1setup!'; Prefix = 'WSLab-'; SwitchName = 'LabSwitch'; DCEdition='4'; Internet=$true ; TelemetryLevel='Full' ; TelemetryNickname='' ; AdditionalNetworksConfig=@(); VMs=@()}
#$LabConfig.VMs += @{ VMName = 'Management' ; ParentVHD = 'Win2019_G2.vhdx'; MGMTNICs=1}
$LabConfig.VMs += @{ VMName = 'Win10' ; ParentVHD = 'Win1020H1_G2.vhdx'; MGMTNICs=1 ; EnableWinRM=$true}
$LabConfig.VMs += @{ VMName = 'Win10_1' ; ParentVHD = 'Win1020H1_G2.vhdx'; MGMTNICs=1 ; EnableWinRM=$true}
#$LabConfig.VMs += @{ VMName = 'Win10_2' ; ParentVHD = 'Win1020H1_G2.vhdx'; MGMTNICs=1 ; EnableWinRM=$true}
The Lab - simple scenario
Download example MSIX package
Run following code from DC or Management machine
$ProgressPreference="SilentlyContinue"
Invoke-WebRequest -Uri "https://github.com/PowerShell/PowerShell/releases/download/v7.0.2/PowerShell-7.0.2-win-x64.msix" -UseBasicParsing -OutFile "$env:USERPROFILE\Downloads\PowerShell-7.0.2-win-x64.msix"
$ProgressPreference="Continue"
Install Hyper-V (to be able to mount-vhd) and reboot
Enable-WindowsOptionalFeature -FeatureName Microsoft-Hyper-V -Online -NoRestart
Install-WindowsFeature -Name Hyper-V-PowerShell
Restart-Computer
Transform MSIX package to VHDx
Download Create_VHDx_from_msix.ps1, right-click it, and run with PowerShell.
Or simplified version of the same
$folder="c:\temp"
#ask for MSIX file(s)
[reflection.assembly]::loadwithpartialname("System.Windows.Forms")
$openFiles = New-Object System.Windows.Forms.OpenFileDialog -Property @{
Multiselect = $true;
Title="Please select msix file(s)"
}
$openFiles.Filter = "msix files (*.msix)|*.msix"
If($openFiles.ShowDialog() -eq "OK"){
Write-Host "Selected Files:" -ForegroundColor Cyan
foreach ($filename in $openFiles.Filenames){
Write-Host "`t $FileName" -ForegroundColor Cyan
}
}
#Download MSIX Image tool if not available"
New-Item -Path $folder -ItemType Directory -ErrorAction Ignore
if (!(Test-Path "$folder\msixmgr\x64\msixmgr.exe")){
Invoke-WebRequest -Uri https://aka.ms/msixmgr -OutFile "$folder\msixmgr.zip"
Expand-Archive -Path "$folder\msixmgr.zip" -DestinationPath "$folder\msixmgr"
}
foreach ($File in $openFiles.FileNames){
$appname=($file | Split-Path -Leaf).TrimEnd(".msix")
if (!(test-path -Path $folder)){
New-Item -Path $folder -ItemType Directory
}
$vhd=New-VHD -SizeBytes 100GB -path $folder\$appname.vhdx -dynamic -confirm:$false
#mount and format VHD
$VHDMount=Mount-VHD $vhd.Path -Passthru
$vhddisk = $vhdmount | Get-Disk
$vhddiskpart = $vhddisk | Initialize-Disk -PartitionStyle GPT -PassThru | New-Partition -UseMaximumSize -AssignDriveLetter | Format-Volume -Filesystem NTFS -AllocationUnitSize 8kb -NewFileSystemLabel $appname
Start-Process -FilePath "$folder\msixmgr\x64\msixmgr.exe" -ArgumentList "-Unpack -packagePath `"$File`" -destination $($vhddiskpart.driveletter):\ -applyacls" -Wait
Dismount-VHD $vhddisk.number
}
As result, you will find VHDx file under temp file (if you used script example above)
Create File Share and copy VHDx there
Assuming you can run it from everywhere, following example is using invoke-command. It will create fileshare, where users and computers have read only access and copies all VHDs from c:\temp into it. Assuming you created your app in c:\temp
#Create new FileShare on DC
$ComputerName="DC"
$FolderName="FileShare"
Invoke-Command -ComputerName $ComputerName -ScriptBlock {new-item -Path c:\Shares -Name $using:FolderName -ItemType Directory}
$accounts=@()
$accounts+="corp\Domain Computers"
$accounts+="corp\Domain Users"
New-SmbShare -Name $FolderName -Path "c:\Shares\$FolderName" -ReadAccess $accounts -CimSession $ComputerName
#Set NTFS permissions
Invoke-Command -ComputerName $ComputerName -ScriptBlock {(Get-SmbShare $using:FolderName).PresetPathAcl | Set-Acl}
#Copy VHDx there
Copy-Item -Path C:\temp\*.vhdx -Destination \\dc\c$\Shares\FileShare
Stage application on Windows 10
This step will mount all VHDs with application located in fileshare in read-only mode and create junction in c:\ProgramData\AppAttach (you can use any location). The next step is, that App will be staged (registered) into c:\program files\windows apps the same way as any other windows app. Since mounting VHD will not survive reboot, it's needed to run this script every reboot, and also every time you add new application into share.
Run this script while logged in Win10 machine. It requires admin permissions. In real world scenario, you would run this code as scheduled task under system or with SCCM.
#https://docs.microsoft.com/en-us/azure/virtual-desktop/app-attach#prepare-powershell-scripts-for-msix-app-attach
$fileshare="\\dc\FileShare"
$msixJunction = "C:\ProgramData\AppAttach\"
#grab all VHDs
$VHDs = Get-ChildItem -Path $fileshare -Name "*.vhd*"
foreach ($vhd in $VHDs){
#region mountvhd
$diskimage=$null
$diskimage=Mount-Diskimage -ImagePath $fileshare\$vhd -NoDriveLetter -Access ReadOnly -PassThru -ErrorAction Ignore
if (!($diskimage)){
Write-Host "Application " -NoNewline
Write-Host "$vhd " -NoNewLine -ForegroundColor Green
Write-Host "VHD was is mounted, app was probably already provisioned"
}else{
#Create Junction link
$msixDest=(Get-Disk -Number $diskimage.Number | Get-Partition | where PartitionNumber -eq 2 | Get-Volume).Path
#$msixDest = "\\?\Volume{" + $volumeGuid + "}\"
if (!(Test-Path $msixJunction)){md $msixJunction}
$packageName=$vhd.TrimEnd(".vhdx")
$junctionpath = $msixJunction + $packageName
cmd.exe /c mklink /j $junctionpath $msixDest
#stage app into c:\program files\windowsapps
$path=(Get-ChildItem -Path $junctionpath | select -First 1).FullName
[Windows.Management.Deployment.PackageManager,Windows.Management.Deployment,ContentType=WindowsRuntime] | Out-Null
Add-Type -AssemblyName System.Runtime.WindowsRuntime
$asTask = ([System.WindowsRuntimeSystemExtensions].GetMethods() | Where { $_.ToString() -eq 'System.Threading.Tasks.Task`1[TResult] AsTask[TResult,TProgress](Windows.Foundation.IAsyncOperationWithProgress`2[TResult,TProgress])'})[0]
$asTaskAsyncOperation = $asTask.MakeGenericMethod([Windows.Management.Deployment.DeploymentResult], [Windows.Management.Deployment.DeploymentProgress])
$packageManager = [Windows.Management.Deployment.PackageManager]::new()
#$path = $msixJunction + $parentFolder + $packageName # needed if we do the pbisigned.vhd
$path = ([System.Uri]$path).AbsoluteUri
$asyncOperation = $packageManager.StagePackageAsync($path, $null, "StageInPlace")
$task = $asTaskAsyncOperation.Invoke($null, @($asyncOperation))
$task
}
}
As you can see, VHD was mounted in read-only mode
It is mounted into "C:\ProgramData\AppAttach" folder (junction link to \?\Volume path)
And then the application exists in "c:\Program Files\WindowsApps"
Register App
Following code will register app for current user, so user will see it in start menu. There can be multiple application names in variable, but in the example is only one. Run code from win10 machine
#register application for user (needs to run under user context
$applications="Microsoft.PowerShellPreview_7.0.2.0_x64__8wekyb3d8bbwe"
foreach ($application in $applications){
$AppxPackagePath = "C:\Program Files\WindowsApps\" + $application + "\AppxManifest.xml"
Add-AppxPackage -Path $AppxPackagePath -DisableDevelopmentMode -Register
}
The Lab - real-world scenario
In real world scenario you need to distribute different application to different users. You also need to make sure, staging is done every time VM starts and also right after you publish new app, it will be available to users.
Run all code from DC or Management machine
Create scheduled task to run AppAttachStaging script
#Save powershell script for AppAttach staging to FileShare
$scriptblock=@'
#https://docs.microsoft.com/en-us/azure/virtual-desktop/app-attach#prepare-powershell-scripts-for-msix-app-attach
$fileshare="\\dc\FileShare"
$msixJunction = "C:\ProgramData\AppAttach\"
#grab all VHDs
$VHDs = Get-ChildItem -Path $fileshare -Name "*.vhd*"
foreach ($vhd in $VHDs){
#region mountvhd
$diskimage=$null
$diskimage=Mount-Diskimage -ImagePath $fileshare\$vhd -NoDriveLetter -Access ReadOnly -PassThru -ErrorAction Ignore
if (!($diskimage)){
Write-Host "Application " -NoNewline
Write-Host "$vhd " -NoNewLine -ForegroundColor Green
Write-Host "VHD was is mounted, app was probably already provisioned"
}else{
#Create Junction link
$msixDest=(Get-Disk -Number $diskimage.Number | Get-Partition | where PartitionNumber -eq 2 | Get-Volume).Path
#$msixDest = "\\?\Volume{" + $volumeGuid + "}\"
if (!(Test-Path $msixJunction)){md $msixJunction}
$packageName=$vhd.TrimEnd(".vhdx")
$junctionpath = $msixJunction + $packageName
cmd.exe /c mklink /j $junctionpath $msixDest
#stage app into c:\program files\windowsapps
$path=(Get-ChildItem -Path $junctionpath | select -First 1).FullName
[Windows.Management.Deployment.PackageManager,Windows.Management.Deployment,ContentType=WindowsRuntime] | Out-Null
Add-Type -AssemblyName System.Runtime.WindowsRuntime
$asTask = ([System.WindowsRuntimeSystemExtensions].GetMethods() | Where { $_.ToString() -eq 'System.Threading.Tasks.Task`1[TResult] AsTask[TResult,TProgress](Windows.Foundation.IAsyncOperationWithProgress`2[TResult,TProgress])'})[0]
$asTaskAsyncOperation = $asTask.MakeGenericMethod([Windows.Management.Deployment.DeploymentResult], [Windows.Management.Deployment.DeploymentProgress])
$packageManager = [Windows.Management.Deployment.PackageManager]::new()
#$path = $msixJunction + $parentFolder + $packageName # needed if we do the pbisigned.vhd
$path = ([System.Uri]$path).AbsoluteUri
$asyncOperation = $packageManager.StagePackageAsync($path, $null, "StageInPlace")
$task = $asTaskAsyncOperation.Invoke($null, @($asyncOperation))
$task
}
}
'@
$scriptblock | out-file \\dc\c$\Shares\FileShare\AppAttachStaging.ps1
#schedule a task on computer win10_1 to run on every system startup.
$ComputerName="Win10_1"
$TaskName="AppAttachStagingTask"
$ScriptPath="\\DC\FileShare\AppAttachStaging.ps1"
$action = New-ScheduledTaskAction -Execute 'PowerShell.exe' -Argument "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -file $ScriptPath"
$trigger=@()
$trigger += New-ScheduledTaskTrigger -AtStartup
$trigger += New-ScheduledTaskTrigger -At "0:00" -RepetitionInterval "00:10" -RandomDelay "00:05"-Once
$task=Register-ScheduledTask -Action $action -TaskName $TaskName -trigger $trigger -CimSession $ComputerName -User "NT Authority\System"
$task.Settings.DisallowStartIfOnBatteries=$false
$settings=$task.settings
Set-ScheduledTask -CimSession $ComputerName -TaskName $TaskName -Settings $settings
Start-ScheduledTask -CimSession $ComputerName -TaskName $TaskName
$task=Get-ScheduledTask -CimSession $ComputerName -TaskName $TaskName
while ($task.State -ne "Ready"){
$task=Get-ScheduledTask -CimSession $ComputerName -TaskName $TaskName
Start-Sleep 1
Write-Host "." -NoNewline
}
#Unregister-ScheduledTask -CimSession $ComputerName -TaskName $TaskName -Confirm:0
#endregion
Generate application list on fileshare
#location of VHDs where i have right to write
$VHDLocation="\\dc\c$\Shares\FileShare"
$vhds=Get-ChildItem -Path $vhdlocation -Name *.vhdx
#mount in read only mode, grab app name and save it into vhd location share
foreach ($vhd in $vhds){
$diskimage=$null
$diskimage=Mount-Diskimage -ImagePath $VHDLocation\$vhd -NoDriveLetter -Access ReadOnly -PassThru -ErrorAction Ignore
$path=(Get-Disk -Number $diskimage.Number | Get-Partition | where PartitionNumber -eq 2 | Get-Volume).Path
#create temp junction
cmd.exe /c mklink /j "$env:temp\TempJunction" $path
#grab app name
$appname=(get-childitem -path "$env:temp\TempJunction").Name
#create file in $VHDLocation
New-Item -Name $appname -Path $VHDLocation -ItemType File -ErrorAction Ignore
#dismount VHD
Dismount-DiskImage -ImagePath $vhdlocation\$vhd | Out-Null
#delete junction
Remove-Item "$env:temp\TempJunction" -Force
}
Strip Permissions from files with 0 size (app names)
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force
Install-Module ntfssecurity -Force
$items=Get-ChildItem -Path '\\dc\c$\shares\FileShare\' | where length -eq 0
foreach ($item in $items){
$item | Disable-NTFSAccessInheritance
$item | Get-NTFSAccess | Remove-NTFSAccess -Account "Corp\Domain Users"
$item | Get-NTFSAccess | Remove-NTFSAccess -Account "BUILTIN\Users"
}
Configure Access based enumeration on FileShare
Configure access based enumeration, so only users who have read access see app names.
$ComputerName="DC"
$Sharename="FileShare"
Set-SmbShare -Name $Sharename -FolderEnumerationMode AccessBased -Force -CimSession $ComputerName
Create task to add apps once user is logged on + every 10 minutes
Create script on fileshare
$scriptblock=@'
$fileshare="\\dc\FileShare"
$applications=(get-childitem -path $fileshare | where length -eq 0).Name
foreach ($application in $applications){
$AppxPackagePath = "C:\Program Files\WindowsApps\" + $application + "\AppxManifest.xml"
Add-AppxPackage -Path $AppxPackagePath -DisableDevelopmentMode -Register
}
'@
$scriptblock | out-file \\dc\c$\Shares\FileShare\AppAttachRegistration.ps1
Schedule task on user logon + every 10 minutes
$ComputerName="Win10_1"
$TaskName="AppAttachRegistrationTask"
$TaskDescription="AppAttach Registration Task"
$ScriptPath="\\DC\FileShare\AppAttachRegistration.ps1"
#First create task that will be available for all users (no chance with PowerShell, COM is needed https://docs.microsoft.com/en-us/windows/win32/taskschd/task-scheduler-objects)
Invoke-Command -ComputerName $ComputerName -ScriptBlock {
$ShedService = New-Object -comobject 'Schedule.Service'
$ShedService.Connect()
$Task = $ShedService.NewTask(0)
$Task.RegistrationInfo.Description = $using:TaskDescription
$Task.Settings.Enabled = $true
$Task.Settings.AllowDemandStart = $true
$Task.Settings.StopIfGoingOnBatteries = $false
$trigger = $task.triggers.Create(9)
$trigger.Enabled = $true
$action = $Task.Actions.Create(0)
$action.Path = 'PowerShell.exe'
$action.Arguments = "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -file $using:ScriptPath"
$taskFolder = $ShedService.GetFolder("\")
$taskFolder.RegisterTaskDefinition($using:TaskName, $Task , 6, 'Users', $null, 4)
}
#modify existing task (add trigger)
$task=Get-ScheduledTask -TaskName $TaskName -CimSession $ComputerName
$triggers=$task.triggers
$triggers+= New-ScheduledTaskTrigger -At "0:00" -RepetitionInterval "00:10" -RandomDelay "00:05"-Once
Set-ScheduledTask -Trigger $triggers -TaskName $TaskName -CimSession $ComputerName
Test
Create 2 users. One with access and one without access on app file name. And login into win10 to see if app was provisioned
Bob will have PS7 and Rob not once they will log in.
New-ADUser -Name Bob -AccountPassword (ConvertTo-SecureString "LS1setup!" -AsPlainText -Force) -Enabled $True -Path "ou=workshop,dc=corp,dc=contoso,dc=com"
New-ADUser -Name Rob -AccountPassword (ConvertTo-SecureString "LS1setup!" -AsPlainText -Force) -Enabled $True -Path "ou=workshop,dc=corp,dc=contoso,dc=com"
#assign read only perm for Bob for Posh7
$appname="Microsoft.PowerShellPreview_7.0.2.0_x64__8wekyb3d8bbwe"
$fileshare="\\dc\c$\shares\Fileshare"
Add-NTFSAccess -Path "$fileshare\$appname" -Account "Corp\Bob" -AccessRights Read
After logon to Win10_1, Bob has PowerShell 7 available in start menu (notice Bob has read access to file - this determines if he should have app or not)
And Rob not. Notice, that app name file is not even visible thanks to Access-based Enumeration.