Deprecated features of the task scheduler

I was reviewing antivirus exclusions for various components – IIS, WSUS, Windows, SQL, ConfigMgr – and found that I’d like to be spammed whenever my server complains that it’s running out of memory…

…not necessarily due to the antivirus (mis)configuration or the fact that my applications are intensively consuming available resources. Actually, it’s a best practice to monitor the performance of servers when you know that the following can happen: (Sorry, I couldn’t resist to publish a selection of my favorites known issues 😉 )

  • Memory leak in the svchost process which hosts the Task Scheduler service:
  • Summary
    When you have a scheduled task configured with the option “Do not store password. The task will only have access to local resources”, each time the task runs, it may leak 4 KB memory (and internally it’s 64 KB) in the svchost.exe instance which hosts the Task Scheduler service.

    This normally won’t be an issue, but if the machine keeps running for long time and the task runs frequently, the leaked memory would become considerable eventually, which may cause the svchost.exe process to be instable.

    As that instance of the svchost.exe process also hosts other services like User Profile Service, you may encounter symptoms like logon failure (get the error message “The User Profile Service service failed the logon. No more threads can be created in the system.”)

    More information
    To work around the issue, you may uncheck the option “Do not store password. The task will only have access to local resources”.

  • High CPU usage on a Windows Server 2012-based server when many client computers connect to the server:
  • Symptoms:
    Assume that many client computers that have unique IP addresses connect to a Windows Server 2012-based server. For example, more than 100,000 client computers that have unique IP addresses connect to the server. In this situation, the network traffic stalls, and the server experiences high CPU usage.

    This issue occurs because the periodic cleanup routine for the local cache holds lots of locks during operations that consume lots of resources.

    Resolution: apply Hotfix

  • The Windows.edb file grows very large in Windows 8 or Windows Server 2012:
  • Symptoms
    In Windows 8 or Windows Server 2012, the Windows Search Service may bloat the Windows.edb file. When this issue occurs, the Windows.edb file grows to a very large size and consumes lots of disk space. In some instances, the file size can be larger than 50 gigabytes (GB).

    Resolution: apply 2836988 Windows 8 and Windows Server 2012 update rollup: May 2013

    Note This update is preventative, but not corrective. To reduce the size of a Windows.edb file that is already affected by this issue, you must rebuild the search index after you install this update.

  • Windows Server 2012: Server Manager can consume a large amount of private memory
  • Symptoms:

    Consider the following scenario:

    You are running Windows Server 2012 and Server Manager is running in one or more sessions
    There is high load on the system and a process or processes are logging a large number of events to the event log on the system within the Server Manager retention period (default 24 hours)

    In the above scenario, Server Manager can continue to consume memory on the system until all memory is exhausted and the server becomes unresponsive.

    The minimum event retention period in Server Manager is 24 hours. Server Manager combines data from various sources within memory. The behavior occurs because Server Manager does not observe Eventlog quotas nor does it release the events from memory when they are outside of the display filters for Server Manager (even with a manual refresh). Server Manager frees the event data from memory after the retention period set by the user. The default retention period is 24 hours.

    Error conditions on the system further exacerbate the issue due to the higher rate of event generation and subsequently a higher rate of memory consumption.


    To resolve this issue, install the Windows 8 and Windows Server 2012 cumulative update 2811660: March 2013

    To workaround this issue, Microsoft recommends investigating the source of the increased event logging and resolving the conditions generating the events.

    Alternatively, closing Server Manager resolves the issue.

Basically, I wanted to receive a ton of mails whenever an anormal resource consumption occurs. To achieve this, I needed to create a scheduled task running whenever an event 2004 is logged. And I found 2 limitations along the road:

  • Sending email from the task scheduler has been deprecated on Windows 2012

  • The Performance Team published today: What’s New in Task Scheduler for Windows 8 & Server 2012 which confirmed that the workaround to send messages is now the Send-MailMessage cmdlet. Perfect, that’s what I did 😎 …(see below).

  • The New-ScheduledTaskTrigger is unable to create a trigger based on events
  • My approach to workaround this issue consists in first creating the task with regular Powershell cmdlets from the ScheduledTasks module, then exporting the task in XML, removing the TimeTrigger childnode, replacing it with an EventTrigger node and updating the XML definition of the task. Well, it isn’t as easy as it sounds… Here’s what I did:

    # Define the command parameter of powershell.exe
    $command = "`"& { Send-MailMessage -From -To -SmtpServer my.smtp.server -Subject 'Resource Exhaustion on myserver' }`""
    # note: to avoid backtick, the command can be encoded
    $A = New-ScheduledTaskAction -Execute $env:systemroot\System32\WindowsPowerShell\V1.0\PowerShell.exe -Argument "-NoProfile -ExecutionPolicy BypPass -Command $command"
    $T = New-ScheduledTaskTrigger -Once -At (Get-Date)
    $P = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount
    # By default we have PT72H = 3 days, set it to 1 hour 
    $S = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Hours 1)
    $D = New-ScheduledTask -Action $A -Principal $P -Trigger $T -Settings $S
    Register-ScheduledTask -TaskName "Resource-Exhaustion" -InputObject $D

    The first step is was the easy part….

    # We need a namespace to use XPath
    $xmlNameTable = new-object System.Xml.NameTable
    $xmlNameSpace = new-object System.Xml.XmlNamespaceManager($xmlNameTable)
    # Get the XML definition of the task as a string
    $xmlstr = (Get-ScheduledTask -TaskPath "\" -TaskName "Resource-Exhaustion"| Export-ScheduledTask)
    # Load the above string as XML document
    $xml = New-Object System.Xml.XmlDocument
    # Replace the chdild node (the timetrigger) by my EventTrigger node:
    ($xml.DocumentElement.SelectSingleNode("//task:Triggers",$xmlNameSpace)).InnerXml = @'
    <EventTrigger xmlns="">
      <Subscription>&lt;QueryList&gt;&lt;Query Id="0" Path="System"&gt;&lt;Select Path="System"&gt;*[System[Provider[@Name='Microsoft-Windows-Resource-Exhaustion-Detector'] and EventID=2004]]&lt;/Select&gt;&lt;/Query&gt;&lt;/QueryList&gt;</Subscription>
    # Update the task using a ComObject
    $TaskService = New-Object -com schedule.service
    $taskDef = $TaskService.GetFolder('\').GetTask('Resource-Exhaustion').Definition
    $taskDef.XmlText = ($xml.OuterXml)

    I don’t know if there’s an easiest way of doing it or simplifying the above code. But, if you know, please leave a comment 🙂

    Powershell Update-Help RSS news feed

    Late June, Powershell MVP Don Jones informed the community that there was a problem with the display of the help after you updated it.

    The Powershell Team responsible for updating help files both fixed the problem and also created a RSS feed to let us know what changed 🙂

    Nice. Isn’t it?

    #Requires -Version 3
    function Get-UpdateHelpNewsFeed {
        try {
            Invoke-RestMethod "" -ErrorAction Stop | 
            Select Title,@{
                    $_.Description -replace "\<br\s?/?\>","`n" -replace "\</?[I|i|b]\s?\>"," "
        } catch {
            Write-Warning -Message "Failed to read RSS feed because $($_.Exception.Message)"

    As my favorite RSS reader is dead, I’ve created the above helper function.
    Now you can do:

    Get-UpdateHelpNewsFeed |             
    Select -First 1 -Property Date,Desc |             
    ft -Wrap -AutoSize

    Piloter sa freebox

    Une fois n’est pas coutume, je vais blogguer en français parce que le sujet de ce billet concerne mon opérateur qui vient récemment de mettre à jour son freebox server et qui propose d’interopérer avec une API documentée sur cette page:

  • Etape 1: découvrir la version de l’API:
  • $FbxApi = Invoke-RestMethod -Uri

    Je reçois une réponse en Json m’indiquant la version de l’API

  • Etape 2: Autoriser une application
  • # Déclarer l'application            
    $AuthJson = @'
       "app_id": "fr.freebox.testapp",
       "app_name": "Test App",
       "app_version": "1.0.0",
       "device_name": "Mon PC"
    $BaseURL = "$($FbxApi.api_base_url)v$([int]$FbxApi.api_version)"            
    # Demander l'autorisation            
    $post = Invoke-RestMethod -Uri "$BaseURL/login/authorize" -Method Post -Body $AuthJson

    A ce stade, une demande est affichée sur l’écran du Freebox server

    Il faut une validation manuelle, la flèche vers la droite signifiant ‘oui’.

  • Etape 3: traquer la progression du processus d’autorisation
  • $statusToken = Invoke-RestMethod -Uri "$BaseURL/login/authorize/$($post.result.track_id)"            
    # Requete GET pour interroger le statut            
    while ($statusToken.result.status -eq "pending") {            
     $statusToken = Invoke-RestMethod -Uri "$BaseURL/login/authorize/$($post.result.track_id)"            
     Start-Sleep -Seconds 1            
    if ($statusToken.result.status -eq "granted") {            
        "Bravo, application autorisée"            
        "le app_token secret à conserver est: {0}" -f $post.result.app_token            
    } else {            
        "Echec: résultat du processus d'autorisation: {0}" -f $statusToken.result.status            

    NB: les étapes 2 et 3 ne sont à réaliser qu’une seule fois (à condition que ça ait fonctioné et que les permissions de l’application n’aient pas été révoquées)

  • Etape 4: ouvrir une session
  • $AppToken= 'mon-app_token-secret-a-conserver'            
    $Challenge = (Invoke-RestMethod -Uri "$BaseURL/login").result.challenge            
    # password = hmac-sha1(app_token, challenge)            
    $hmacsha = New-Object System.Security.Cryptography.HMACSHA1            
    $hmacsha.key = [Text.Encoding]::ASCII.GetBytes($AppToken)            
    $signature = $hmacsha.ComputeHash([Text.Encoding]::ASCII.GetBytes($Challenge))            
    $password = [string]::join("", ($signature | % {([int]$_).toString('x2')}))            
    $SessionJson = @"
       `"app_id`": `"fr.freebox.testapp`",
       `"password`": `"$($password)`"
    $session = Invoke-RestMethod -Uri "$BaseURL/login/session/" -Method Post -Body $SessionJson            
    'ouverture de la session avec succes: {0}' -f $session.success             
    'le session_token est: {0}' -f $session.result.session_token            
    # Afficher les permissions            

  • Etape 5: faire un appel authentifié à l’API
  • J’ai choisi un exemple qui consiste à afficher les journaux d’appel.

    # Get-Unixdate from            
    Function Get-Unixdate ($UnixDate){            
      ([datetime]'01/01/1970 00:00:00').AddSeconds($UnixDate)            
    # Construction de l'entête            
    $Header = @{'X-Fbx-App-Auth' = $($session.result.session_token)}            
    # Afficher le journal d'appel            
    (Invoke-RestMethod -Uri "$BaseURL/call/log/" -Headers $Header).result |             
    Sort -Descending:$false -Property datetime |             
    Select name,type,duration,@{l='Date';e={            
    Get-Unixdate $_.datetime            
    }} | ft -AutoSize

    Super, maintenant je peux facilement calculer combien de temps ma femme et mes enfants passent au téléphone 😉

    WMI error 0x8004106C: Quota violation(while running queries)

    I’ve encountered this error on my old System Center Configuration Manager 2007 while I was running intensive WMI queries.

    The error message on quota violation is raised because the WMI provider of the operating system (Windows 2003) had its amount of private memory that can be held by each host set to a 128MB limit.

    To fix this, I set to 512MB which is now the default on Windows 2008/2008R2/2012.

    I think that at that time I followed the guidance that you can find in the following knowledge base article: KB2404366

    To view the settings of the WMI provider, you can query the __ProviderHostQuotaConfiguration WMI class:

    $WMIHT = @{            
     NameSpace=  'root'            
     Class = '__ProviderHostQuotaConfiguration'            
    Get-WmiObject -Computer SCCM2007 @WMIHT


    I’d recommend to stick to the piece of advice “do not modify these quotas for the sake of modifying them!” in the above link. In case you do, you can this way:

    $WMIProviderConfig = Get-WmiObject @WMIHT            
    $WMIProviderConfig.MemoryPerHost = 1024MB            
    try {            
      $WMIProviderConfig.Put() | Out-Null            
      Write-Verbose -Message "Successfully changed the WMI provider settings" -Verbose            
    } catch {            
      Write-Warning "Failed to modify the WMI provider because $($_.Exception.Message)"            

    Note that a reboot is required.

    Changing the ConfigMgr client health evaluator task (CCMEval)

    My ConfigMgr 2012 book for training 10747A: Administering System Center 2012 Configuration Manager says on page 3-53 that:

    This task runs ccmeval.exe at a time between 12:00AM and 1:00AM.

    Ok, it’s fine to set a random time but not between midnight and 1:00AM…to avoid all computers sending info the Management Point at the same time.
    Ok, the task is configured to run ASAP whenever a schedule is missed… for computers shut down at midnight and booting up in the morning.

    If you’d like to introduce more randomness for this task, you can achieve it like this:

    #Requires -Version 2
    Function Set-CMCCMevalTaskTrigger {
        Change the execution time of the CCMEval task
        Change the execution time of the CCMEval task by either setting a random start hour or a specified one
    .PARAMETER ComputerName
        Array of string that represents the remote computers to target
        Integer that sets the hour when the task will execute
    .PARAMETER Random
        Switch to turn of a randomly chosen hour
        Set-CMCCMevalTaskTrigger -Verbose
        Change to execution time on the local computer to 12:00AM instead of 12:00PM
        "RemotePC1","RemotePC2" | Set-CMCCMevalTaskTrigger -Hour 13 -Verbose
        Change to execution time on the specified computers to 01:00PM
        "RemotePC1","RemotePC2" | Set-CMCCMevalTaskTrigger -Random -Verbose
        Change to execution time on the specified computers to a random start time (hour)
    [int]$Hour = 12,
        # Make sure we run as admin...
        $usercontext = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()
        # international mode
        $IsAdmin = $usercontext.IsInRole(544)
        if (-not($IsAdmin)) {
            Write-Warning "Must run powerShell as Administrator to perform these WMI queries"
        $TASK_UPDATE_FLAG = 0x4
        if ($PSBoundParameters.ContainsKey('Random')) {
            $Hour = (Get-Random -Maximum 23 -Minimum 0)
            $NewTime = 'T{0:00}:' -f $Hour
        } else {
            $NewTime = 'T{0:00}:' -f $Hour
    Process {
        $ComputerName | ForEach-Object -Begin {
            $TaskService = New-Object -com schedule.service
        } -Process {
            $Computer = $_
            try {
                # Connect to target computer
                # Get the XML definition of the task
                $taskDef = $TaskService.GetFolder('\Microsoft\Configuration Manager').GetTask('Configuration Manager Health Evaluation').Definition
                # Loop through Calendar triggers to change the start time
                $taskDef.Triggers | ForEach-Object -Process {
                    if ($_.StartBoundary) {
                        $_.StartBoundary = ($_.StartBoundary -replace "T\d{2}:",$NewTime)
                # Update the existing task
                $TaskService.GetFolder('\Microsoft\Configuration Manager').RegisterTaskDefinition(
                  'Configuration Manager Health Evaluation',
                Write-Verbose -Message "Successfully set CCMeval taks execution time to $Hour on $Computer"
            } catch {
                Write-Warning -Message "Failed to change CCMeval task for computer $Computer because $($_.Exception.Message)"
    End {}

    Disabling the system restore

    In a managed environment, System Restore should be used only rarely. In addition, System Restore will not help you find the root cause of a system failure or solve a failure. In managed environments, it is better to have a test environment in which to reproduce the failure and determine the root cause so that the changes can be made in a company-wide scenario.

    It can actually be disabled:

    • during the installation using an unattend configuration file:
    • by group policy

    • Enabling the 2 above settings will write the following values in the registry into the key: HKLM\SOFTWARE\Policies\Microsoft\Windows NT\SystemRestore

    • afterward with Powershell
    • Although Powershell has a built-in cmdlet named Disable-ComputerRestore

      Disable-ComputerRestore -Drive "C:\" -Verbose

      …the following way of disabling may be prefered:

      try {            
          # Disable SR on all drives            
          # Disable it in the registry            
          Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\SystemRestore" -Name DisableSR -Value 1 -Type DWORD -ErrorAction Stop            
          # Also turn off the scheduled task associated with the SR            
          $TaskService = New-Object -com schedule.service            
          $TaskService.GetFolder('\Microsoft\Windows\SystemRestore').GetTask('SR').Enabled = $false            
      } catch {            
          Write-Warning -Message "Failed to turn off the system restore"            

      NB: Administrative rights are required to perform this task.

    More on The Registry Keys and Values for the System Restore Utility:

    Creating a WinPE bootable image with Powershell 4

    Prerequisites: I did all the following from a Windows 8.1 preview that has Powershell 4.0 installed by default

    Step 1: Download ADK 8.1

    #Requires -Version 4
    #Requires -RunAsAdministrator 
    Function Get-ADKFiles {
    Begin {
        $HT = @{}
        $HT += @{ ErrorAction = 'Stop'}
        # Validate target folder
        try {
            Get-Item $TargetFolder @HT | Out-Null
        } catch {
            Write-Warning -Message "The target folder specified as parameter does not exist"
    Process {
        $adkGenericURL = (Invoke-WebRequest -Uri -MaximumRedirection 0 -ErrorAction SilentlyContinue)
        # There's an expected error saying:
        # The maximum redirection count has been exceeded. 
        # To increase the number of redirections allowed, supply a higher value to the -MaximumRedirection parameter.
        # 302 = redirect as moved temporarily
        if ($adkGenericURL.StatusCode -eq 302) {
            # Currently set to
            $MainURL = $adkGenericURL.Headers.Location
            $AllURLs = DATA {
                ConvertFrom-StringData @'
                    0=Toolkit Documentation-x86_en-us.msi
                    6=Application Compatibility Toolkit-x86_en-us.msi
                    11=Microsoft Compatibility Monitor-x86_en-us.msi
                    12=Application Compatibility Toolkit-x64_en-us.msi
                    14=Microsoft Compatibility Monitor-x86_en-us.msi
                    15=Windows Deployment Tools-x86_en-us.msi
                    43=Windows System Image Manager on amd64-x86_en-us.msi
                    44=Windows System Image Manager on x86-x86_en-us.msi
                    45=Windows Deployment Customizations-x86_en-us.msi
                    52=Windows PE x86 x64-x86_en-us.msi
                    57=Windows PE x86 x64 wims-x86_en-us.msi
                    60=User State Migration Tool-x86_en-us.msi
                    63=Volume Activation Management Tool-x86_en-us.msi
                    67=WPT Redistributables-x86_en-us.msi
                    71=Windows Assessment Toolkit-x86_en-us.msi
                    75=Windows Assessment Toolkit (AMD64 Architecture Specific)-x86_en-us.msi
                    76=Windows Assessment Toolkit (X86 Architecture Specific)-x86_en-us.msi
                    77=Assessments on Client-x86_en-us.msi
                    140=Windows Assessment Services-x86_en-us.msi
                    144=Windows Assessment Services - Client (Server SKU)-x86_en-us.msi
                    147=Windows Assessment Services - Client (AMD64 Architecture Specific, Server SKU)-x86_en-us.msi
                    148=Assessments on Server-x86_en-us.msi
                    150=Windows Assessment Services - Client (Client SKU)-x86_en-us.msi
                    151=Windows Assessment Services - Client (X86 Architecture Specific, Client SKU)-x86_en-us.msi
                    152=Windows Assessment Services - Client (AMD64 Architecture Specific, Client SKU)-x86_en-us.msi
                    153=Kits Configuration Installer-x86_en-us.msi
            # Create target folders if required as BIT doesn't accept missing folders
            If (-not(Test-Path (Join-Path -Path $TargetFolder -ChildPath Installers))) {
                try {
                    New-Item -Path (Join-Path -Path $TargetFolder -ChildPath Installers) -ItemType Directory -Force @HT
                    # New-Item -Path $TargetFolder -ItemType Directory -Force -ErrorAction Stop
                } catch {
                    Write-Warning -Message "Failed to create folder $($TargetFolder)/Installers"
            # Get adksetup.exe
            iwr -Uri "$($MainURL)adksetup.exe" -OutFile  "$($TargetFolder)\adksetup.exe"
            # Create an job that will downlad our first file
            $job = Start-BitsTransfer -Suspended -Source "$($MainURL)Installers/$($AllURLs['0'])" -Asynchronous -Destination (Join-Path -Path $TargetFolder -ChildPath ("Installers/$($AllURLs['0'])")) 
            For ($i = 1 ; $i -lt $AllURLs.Count ; $i++) {
                $URL = $Destination = $null
                $URL = "$($MainURL)Installers/$($AllURLs[$i.ToString()])"
                $Destination = Join-Path -Path (Join-Path -Path $TargetFolder -ChildPath Installers) -ChildPath (([URI]$URL).Segments[-1] -replace '%20'," ")
                # Add-BitsFile
                $newjob = Add-BitsFile -BitsJob $job -Source  $URL -Destination $Destination
                Write-Progress -Activity "Adding file $($newjob.FilesTotal)" -Status "Percent completed: " -PercentComplete (($newjob.FilesTotal)*100/($AllURLs.Count))
            # Begin the download and show us the job
            Resume-BitsTransfer  -BitsJob $job -Asynchronous
            while ($job.JobState -in @('Connecting','Transferring','Queued')) {
                Write-Progress -activity "Downloading ADK files" -Status "Percent completed: " -PercentComplete ($job.BytesTransferred*100/$job.BytesTotal)
            Switch($job.JobState) {
             "Transferred" {
                Complete-BitsTransfer -BitsJob $job
             "Error" {
                # List the errors.
                $job | Format-List 
            default {
                # Perform corrective action.
    End {}
    # Dotsource the above script            
    . C:\Get-ADK81.ps1            
    # Download ADK 8.1            
    Get-ADKFiles -TargetFolder C:\ADK.8.1            

    Step 2: Install ADK 8.1

    $MyADKPath = "C:\ADK.8.1"            
    If (Test-Path -Path "$MyADKPath\adksetup.exe") {            
        & (gcm "$MyADKPath\adksetup.exe") @(            
         "/installpath", "${env:ProgramFiles(x86)}\Windows Kits\8.1",            
         "OptionId.DeploymentTools OptionId.WindowsPreinstallationEnvironment",            
    $adkinstall = (Get-Process -Name "adksetup")[0]            
    while (-not($adkinstall.HasExited)) {            
        Get-Content -Path "$env:TEMP\ADKsetup.log" -Tail 1             
     Start-Sleep -Seconds 1            

    Step 3: Create a custom WinPE wim image

    …and its ISO file as well to be either burnt on a CD/DVD or that can be used as a bootable media of a virtual machine 🙂

    $Arch = "x64"
    $TargetArch = "amd64"
    $PEFolder = "C:\WINPE_$($Arch)"
    $WAIKLOCATION = "${env:ProgramFiles(x86)}\Windows Kits\8.1\Assessment and Deployment Kit"
    if (Test-Path $WAIKLOCATION) {
        $PEFolder,"$PEFolder\ISO","$PEFolder\ISO\Sources","$PEFolder\mount" | ForEach-Object {
            if (-not(Test-Path $_ -PathType Container)) {
                try {
                    New-Item -Path $_ -ItemType Container -ErrorAction Stop -Force
                } catch {
                    Write-Warning -Message "Failed to create target folders"
        try {
            Copy-Item -Path "$WAIKLOCATION\Windows Preinstallation Environment\$TargetArch\Media\bootmgr*" -Destination "$PEFolder\ISO\" -Force -ErrorAction Stop
            Copy-Item -Path "$WAIKLOCATION\Deployment Tools\$TargetArch\Oscdimg\" -Destination $PEFolder -Force -ErrorAction Stop
            Copy-Item -Path "$WAIKLOCATION\Deployment Tools\$TargetArch\Oscdimg\efisys.bin" -Destination $PEFolder -Force -ErrorAction Stop
            & (gcm "$env:systemroot\system32\robocopy.exe") @("$WAIKLOCATION\Windows Preinstallation Environment\$TargetArch\Media\boot","$PEFolder\ISO\boot",'/S','/r:0','/Z','/PURGE') | Out-Null
            Copy-Item -Path "$WAIKLOCATION\Windows Preinstallation Environment\$TargetArch\en-us\winpe.wim" -Destination $PEFolder -ErrorAction Stop -Force
        } catch {
            Write-Warning "Copy Failed"
        try {
            # Mount the image
            Get-WindowsImage -ImagePath "$PEFolder\winpe.wim" -ErrorAction Stop | ForEach-Object {
                Mount-WindowsImage -Path "$PEFolder\mount" -ErrorAction Stop -Index $_.ImageIndex -ImagePath $_.ImagePath
            # dism.exe /Get-MountedWimInfo
            Get-WindowsImage -Mounted -ErrorAction Stop
            # Add Packages
            "HTA","Scripting","WMI","Dot3Svc","NetFx","PowerShell","DismCmdlets" | ForEach-Object {
                Add-WindowsPackage -Path "$PEFolder\mount" -PackagePath "$WAIKLOCATION\Windows Preinstallation Environment\$TargetArch\WinPE_OCs\winpe-$" -ErrorAction Stop
            # Set the keyboard layout to French
            & (gcm "$WAIKLOCATION\Deployment Tools\$TargetArch\DISM\dism.exe") @("/image:$PEFolder\mount",'/Set-InputLocale:040c:0000040c')
            # Increase the writable memory space
            & (gcm "$WAIKLOCATION\Deployment Tools\$TargetArch\DISM\dism.exe") @("/image:$PEFolder\mount",'/Set-ScratchSpace:256')
            # Set the time zone to GMT+1 Brussels, Copenhagen, Madrid, Paris
            & (gcm "$WAIKLOCATION\Deployment Tools\$TargetArch\DISM\dism.exe") @("/image:$PEFolder\mount",'/Set-TimeZone:Romance Standard Time')
            # Unmount
            Get-WindowsImage -Mounted -ErrorAction Stop | ForEach-Object {
                # Save-WindowsImage -Path $_.ImagePath
                Dismount-WindowsImage -Path $_.Path -Save -ErrorAction Stop
            # Create the ISO file    
            Copy-Item -Path "$PEFolder\winpe.wim" -Destination "$PEFolder\ISO\Sources\boot.wim" -ErrorAction Stop
            $BOOTDATA='2#p0,e,b"{0}"#pEF,e,b"{1}"' -f "$PEFolder\","$PEFolder\efisys.bin"
            & (gcm "$WAIKLOCATION\Deployment Tools\amd64\Oscdimg\oscdimg.exe") @("-bootdata:$BOOTDATA",'-u1','-udfver102',"$PEFolder\ISO","$PEFolder\winpe.iso")
        } catch {
            Write-Warning "Failed to create ISO image"

    Step 4: Prepare a USB stick

    # Erase all the data on the USB stick ~ diskpart / clean            
    (Get-Disk)[-1] | Clear-Disk -RemoveData:$true            

    (Get-Disk)[-1] | Get-Partition            
    # Create an active partition            
    (Get-Disk)[-1] | New-Partition -UseMaximumSize:$true -IsActive -Verbose            

    # List the new volume            
    (Get-Disk)[-1] | Get-Partition | Get-Volume            
    # Simulate the format of the volume            
    (Get-Disk)[-1] | Get-Partition | Get-Volume | Format-Volume -FileSystem NTFS -WhatIf            
    # Format the new partition            
    (Get-Disk)[-1] | Get-Partition | Get-Volume | Format-Volume -FileSystem NTFS

    If the partition doesn’t get a drive letter assigned automatically, you can do:

    (Get-Disk)[-1] | Get-Partition  | Set-Partition -NewDriveLetter Z

    Move the content to the USB media:

    robocopy --% C:\WINPE_x64\ISO Z:\ /S /r:0 /Z

    Booting from the ISO file a VM with 1024MB memory and no HDD attached