Build a Complete Solution

Applies To: Windows 8.1

For a Windows RT 8.1 deployment in education you can use scripts to build a complete solution to prepare shared devices and personal devices for delivery.

The sample scripts in the Create the Configuration Store section showed how to solve individual problems, such as applying updates or scheduling tasks. The scripts in this section combine everything into a complete solution, including scripts to stock the configuration store and apply shared and personal configurations.

You must stock most of the files in the configuration store manually. Copy APPX files to the Apps subfolder, MSU files to the Updates subfolder, and so on. However, Listing 15 and Listing 16 automatically copy local Group Policy settings and wireless networking profiles from the reference device to the configuration store. Listing 15 is a batch script that runs the similarly named Windows PowerShell script while bypassing execution policy, preventing installers from having to set execution policy to unrestricted on each device they configure.

Store both scripts in the Scripts subfolder of the configuration store so that you can access them from any reference device. You specify the path to the configuration store in the last line of Listing 15.

Listing 15. Gather-DeviceConfig.cmd

@echo off

rem Gather-DeviceConfig.cmd
rem
rem Start Gather-DeviceConfig.ps1, bypassing execution policy.

powershell.exe -ExecutionPolicy Bypass %~dp0Gather-DeviceConfig.ps1 -StorePath D:\Store

Listing 16. Gather-DeviceConfig.ps1

# Gather-DeviceConfig.ps1
# Gather configuration from reference device and save in the configuration store.
# The configuration store can be on a USB flash drive or a network share. See the
# guide "Deploying Windows RT 8.1 in education" for more information about using
# and customizing this script to configure Windows RT devices in schools.
# DISCLAIMER
# ----------
# This sample script is not supported under any Microsoft standard support program
# or service. This sample script is provided AS IS without warranty of any kind.
# Microsoft further disclaims all implied warranties including, without limitation,
# any implied warranties of merchantability or of fitness for a particular purpose.
# The entire risk arising out of the use or performance of this sample script and
# documentation remains with you. In no event shall Microsoft, its authors, or anyone
# else involved in the creation, production, or delivery of this sample script be
# liable for any damages whatsoever (including, without limitation, damages for loss
# of business profits, business interruption, loss of business information, or other
# pecuniary loss) arising out of the use of or inability to use this sample script or
# documentation, even if Microsoft has been advised of the possibility of such damages.

param (
    [Parameter(Mandatory=$true, HelpMessage = "Path to the folder containing the configuration store")][string] $StorePath
)

$ErrorActionPreference = 'Stop'
if (!(Test-Path -Path $StorePath -PathType Container)) {
    throw "$StorePath was not found."
}

# GLOBAL VARIABLES ##############################################

# If you change the following folder and file names, you must also change them in
# Apply-SharedConfig.pst, Apply-PersonalConfig.pst, and Update-DeviceConfig.pst.

$PoliciesPath = Join-Path $StorePath "Policies"
$ProfilesPath = Join-Path $StorePath "Profiles"

# FUNCTIONS #####################################################

function Gather-WirelessProfiles {

    # Export all wireless profiles on the device to the given $Path.

    param (
        [Parameter(Mandatory=$true, HelpMessage = "Path to the folder in which to store wireless profiles")][string] $Path
    )

    if (!(Test-Path $Path)) {
        throw "Unable to export wireless profiles. $Path was not found."
    }

    Write-Output "Gathering wireless profiles to $Path."

    netsh.exe wlan export profile folder="$Path" key=clear | Tee-Object -Variable Results | Out-Null
    if ($LASTEXITCODE -ne 0) {
        throw $Results
    }

    Write-Output "Finished gathering wireless profiles to $Path."
}

function Gather-GroupPolicy {

    # Capture local Group Policy and save in the configuration store.
    # Important: Make sure that the path and file name of $StartLayoutFile
    #            is the same as used in the Start Menu Layout Group Policy setting.
    #            By default, these scripts create the file layout.xml in the path
    #            C:\Windows\System32\GroupPolicy. For more information, see 
    #            "Deploying Windows RT 8.1 in education."

    param (
        [Parameter(Mandatory=$true, HelpMessage = "Path to the folder in which to store the local Group Policy object")][string] $Path
    )
    $PolicySource    = "C:\Windows\System32\GroupPolicy"
    $SecurityInfFile = Join-Path $PolicySource "security.inf"
    $StartLayoutFile = Join-Path $PolicySource "layout.xml"

    Write-Output "Gathering Group Policy settings to $Path."

    secedit /export /cfg $SecurityInfFile | Tee-Object -Variable Results | Out-Null
    if ($LASTEXITCODE -ne 0) {
        throw $Results
    }

    Export-StartLayout –path $StartLayoutFile –as XML
    xcopy $PolicySource\*.* $Path\*.* /s /d /h /r /y  | Tee-Object -Variable Results | Out-Null
    if ($LASTEXITCODE -ne 0) {
        throw $Results
    }

    Write-Output "Finished gathering Group Policy settings to $Path"
}

# MAIN ##########################################################

Write-Output "Beginning to gather this device's configuration."
Write-Output "------------------------------------------------------------------------"
Gather-GroupPolicy $PoliciesPath
Write-Output "------------------------------------------------------------------------"
Gather-WirelessProfiles $ProfilesPath
Write-Output "------------------------------------------------------------------------"
Write-Output "Finished gathering this device's configuration."

Preparing shared devices for delivery

Similar to the scripts in the previous section, Listing 17 and Listing 18 are complete examples that rely on the examples you learned about in the Create the Configuration Store section. Listing 17 runs the Windows PowerShell script in Listing 18 while bypassing execution policy. Listing 18 is a working example that applies the contents of the previously prepared configuration store to the target device.

Store both scripts in the Scripts subfolder of the configuration store so that you can access them from any target device. You specify the path to the configuration store in the last line of Listing 17.

In shared-device scenarios, the preparation process is as follows:

  1. Start the device, and complete the OOBE.

  2. At an elevated command prompt, run the code Listing 17, which launches the Windows PowerShell script in Listing 18 while bypassing execution policy.

  3. Perform any manual steps required to configure the device (for example, installing Windows Store apps).

  4. Shut down the device, and deliver it to the classroom.

Listing 17. Apply-SharedConfig.cmd

@echo off

rem Apply-SharedConfig.cmd
rem
rem Start Apply-SharedConfig.ps1, bypassing execution policy.

powershell.exe -ExecutionPolicy Bypass %~dp0Apply-SharedConfig.ps1 -StorePath D:\Store

Listing 18. Apply-SharedConfig.ps1

# Apply-SharedConfig.ps1
# Apply settings from the configuration store to the local device.
# This script configures shared devices. See Apply-PersonalConfig.ps1
# for a script that prepares devices for one-to-one scenarios. See the guide
# "Deploying Windows RT 8.1 in education" for more information about using
# and customizing this script to configure Windows RT devices in schools.
# DISCLAIMER
# ----------
# This sample script is not supported under any Microsoft standard support program
# or service. This sample script is provided AS IS without warranty of any kind.
# Microsoft further disclaims all implied warranties including, without limitation,
# any implied warranties of merchantability or of fitness for a particular purpose.
# The entire risk arising out of the use or performance of this sample script and
# documentation remains with you. In no event shall Microsoft, its authors, or anyone
# else involved in the creation, production, or delivery of this sample script be
# liable for any damages whatsoever (including, without limitation, damages for loss
# of business profits, business interruption, loss of business information, or other
# pecuniary loss) arising out of the use of or inability to use this sample script or
# documentation, even if Microsoft has been advised of the possibility of such damages.

param (
    [Parameter(Mandatory=$true, HelpMessage = "Path to the folder containing the configuration store")][string] $StorePath
)

$ErrorActionPreference = 'Stop'
if (!(Test-Path -Path $StorePath -PathType Container)) {
    throw "$StorePath was not found."
}

# GLOBAL VARIABLES ##############################################

# The following variables define subfolder names within
# the configuration store. These scripts expect specific types
# of files to appear in specific subfolders. If you change the
# following folder and file names, you must also change them in
# Gather-DeviceConfig.pst, Apply-PersonalConfig.pst, and
# Update-DeviceConfig.pst.

$AppsPath        = Join-Path $StorePath "Apps"
$FilesPath       = Join-Path $StorePath "Files"
$LogsPath        = Join-Path $StorePath "Logs"
$PoliciesPath    = Join-Path $StorePath "Policies"
$ProfilesPath    = Join-Path $StorePath "Profiles"
$SettingsPath    = Join-Path $StorePath "Settings"
$TasksPath       = Join-Path $StorePath "Tasks"
$UpdatesPath     = Join-Path $StorePath "Updates"

# The following variables define the user name and password
# to use to create scheduled tasks on each device. The scripts
# will add this account to the local device and use it when
# creating each scheduled task.

$TaskUser        = "DevAdmin"
$TaskPassword    = "Passw0rd"

# Additional variables:

$Interface       = "Wi-Fi"       # The name of the Wi-Fi interface on Surface devices
$UserPassword    = "Passw0rd"    # The password to use when creating the shared user account
$FilesTarget     = "C:"          # The root of the file system for applying local files.

# FUNCTIONS #####################################################

function Apply-AppxPackages {

    # Install each Windows Store app from the configuration store.
    # Make sure the Group Policy setting AllowAllTrustedApps is enabled
    # and a sideloading product key is installed on the device.

    param (
        [Parameter(Mandatory=$true, HelpMessage = "Path of the folder containing Windows Store app (APPX) packages")][string] $Path
    )

    if ((Test-Path -Path $Path -PathType Container)) {

        Push-Location -Path $Path
    
        $AppPackages = Get-ChildItem -Filter *.appx
    
        $PackageCount = ($AppPackages | Measure-Object).Count
        Write-Output "Installing ($PackageCount) apps from the configuration store."

        $AppPackages | ForEach-Object {
            Write-Output "...$_"
            Add-AppxProvisionedPackage -Online -PackagePath $_ -SkipLicense
        }

        Pop-Location
        Write-Output "Finished installing app packages on the device."
    }
    else {
        Write-Output "Skipping Windows Store apps because path was not found."
    }
}

function Apply-LocalFiles {

    # Copy files and folders from the configuration store.

    param (
        [Parameter(Mandatory=$true, HelpMessage = "Path of the folder containing folders and files to copy")][string] $Path,
        [Parameter(Mandatory=$true, HelpMessage = "Target path to which to copy the source folders and files")][string] $Target
    )

    if ((Test-Path -Path $Path -PathType Container)) {
        Write-Output "Applying files and folders to this device."
        xcopy.exe $Path\*.* $Target\*.* /s /d /e /h /r /k /y | Tee-Object -Variable Results | Out-Null
        if ($LASTEXITCODE -ne 0) {
            throw $Results
        }
        Write-Output "Finished applying files and folders to this device."
    }
    else {
        Write-Output "Skipping local files because path was not found."
    }
}

Function Log-DeviceWithMac {

    # Create a file containing the computer name, MAC address
    # of the first Wi-Fi adapter, and the device's serial number.

    param (
        [Parameter(Mandatory=$true, HelpMessage = "Path in which to log the computer's name, MAC address, and serial number")][string] $Path
    )

    if ((Test-Path -Path $Path -PathType Container)) {
        Write-Output "Logging the computer name and MAC address in the configuration store."

        $FileName = $env:ComputerName + "_" + $((Get-NetAdapter -Name "Wi-FI").MacAddress ) + ".txt"
        $FullFilePath = Join-Path $Path $FileName
    
        # Check if the file exists do not write a new file, otherwise write the file.

        If (!(Test-Path $FullFilePath)) {
            $Content = $env:ComputerName + ", " + $((Get-NetAdapter -Name "Wi-FI").MacAddress ) + ", " + (Get-WmiObject -Class Win32_BIOS).SerialNumber
            Add-Content -Path $FullFilePath -Value $Content
            Write-Output "...$Content"
        }
        Write-Output "Finished logging the computer name and MAC address in the configuration store."
    }
    else {
        Write-Output "Did not log the device because path was not found."
    }
}

function Enable-GroupPolicy {

    # Enable and start the Group Policy service.

    Set-Service -Name gpsvc -StartupType auto
    Start-Service -Name gpsvc
}

function Apply-GroupPolicy {

    # Apply local Group Policy settings from A configuration store
    # to the local computer, and start the Group Policy service.

    param (
        [Parameter(Mandatory=$true, HelpMessage = "Path of the folder containing the local Group Policy object to copy")][string] $Path
    )
    $Target          = "C:\Windows\System32\GroupPolicy"
    $SecurityInfPath = Join-Path $Target "security.inf"
    $SecuritySdbPath = Join-Path $Target "secedit.sdb"

    if ((Test-Path -Path $Path -PathType Container)) {
        Write-Output "Configuring Group Policy on this device."

        Write-Output "...Copying policy settings to the device."
        xcopy $Path\*.* $Target\*.* /s /d /h /r /y | Tee-Object -Variable Results | Out-Null
        if ($LASTEXITCODE -ne 0) {
            throw $Results
        }

        Write-Output "...Configuring security policy on the device."
        secedit /configure /db $SecuritySdbPath /cfg $SecurityInfPath | Out-Null

        Write-Output "...Enabling and starting the Group Policy service."
        Enable-GroupPolicy

        Write-Output "...Updating Group Policy on the device."
        gpupdate /force | Tee-Object -Variable Results | Out-Null
        if ($LASTEXITCODE -ne 0) {
            throw $Results
        }

        Write-Output "Finished configuring Group Policy on the device."
    }
    else {
        Write-Output "Skipping Group Policy because path was not found."
    }
}

function Apply-RegFiles {

    # Import each registry file found in the configuration store.

    param (
        [Parameter(Mandatory=$true, HelpMessage = "Path of the folder containing registry (REG) files to import on the device")][string] $Path
    )

    if ((Test-Path -Path $Path -PathType Container)) {
        Push-Location -Path $Path
    
        $RegFiles = Get-ChildItem -Filter *.reg
    
        $RegFileCount = ($RegFiles | Measure-Object).Count
        Write-Output "Importing ($RegFileCount) REG files from the configuration store."

        $RegFiles | ForEach-Object {
            Write-Output "...$_"
            reg import $_ | Tee-Object -Variable Results | Out-Null
            if ($LASTEXITCODE -ne 0) {
                throw $Results
            }
        }

        Pop-Location
        Write-Output "Finished importing REG files from the configuration store."
    }
    else {
        Write-Output "Skipping settings because path was not found."
    }
}

function Import-ScheduledTasks {

    # Install each task found in the configuration store.

    param (
        [Parameter(Mandatory=$true, HelpMessage = "Path of the folder containing task (XML) files to import into scheduled tasks")][string] $Path,
        [Parameter(Mandatory=$true, HelpMessage = "Name of the account under which to run each imported scheduled task")][string] $TaskUser,
        [Parameter(Mandatory=$true, HelpMessage = "Password for the account under which to run each imported scheduled task")][string] $TaskPassword
    )

    if ((Test-Path -Path $Path -PathType Container)) {
        Push-Location -Path $Path
    
        # Create the local administrator account to use for running the tasks.

        Write-Output "Creating the local administrator account for $TaskUser."
        net user $TaskUser $TaskPassword /add /expires:never /passwordchg:no | Tee-Object -Variable Results | Out-Null
        if ($LASTEXITCODE -ne 0) {
            throw $Results
        }
        net localgroup "Administrators" $TaskUser /add | Tee-Object -Variable Results | Out-Null
        if ($LASTEXITCODE -ne 0) {
            throw $Results
        }

        # Add each task file to the task scheduler, using our local administrator account.

        $TaskFiles = Get-ChildItem -Filter *.xml
    
        $TaskFileCount = ($TaskFiles | Measure-Object).Count
        Write-Output "Importing ($TaskFileCount) scheduled tasks from the configuration store."

        $TaskFiles | ForEach-Object {
            Write-Output "...$_"
            $TaskXML = get-content $_ | Out-String
            Register-ScheduledTask -Xml $TaskXML -TaskName $_ -User $TaskUser -Password $TaskPassword
        }

        Pop-Location
        Write-Output "Finished importing scheduled tasks from the configuration store."
    }
    else {
        Write-Output "Skipping tasks because path was not found."
    }
}

function Apply-UpdateFiles {

    # Install each update found in the configuration store. Windows RT does
    # not support WSUS and an update catalog is not available. Contact your
    # account team about acquiring update packages (MSU files).

    param (
        [Parameter(Mandatory=$true, HelpMessage = "Folder containing Microsoft update (MSU) files to install on the device")][string] $Path
    )

    if ((Test-Path -Path $Path -PathType Container)) {
        Push-Location -Path $Path
    
        $UpdatePackages = Get-ChildItem -Filter *.msu
    
        $PackageCount = ($UpdatePackages | Measure-Object).Count
        Write-Output "Installing ($PackageCount) updates from the configuration store."

        $UpdatePackages | ForEach-Object {
            Write-Output "...$_"
            $cmd = "wusa $_ /quiet /norestart"
            Invoke-Expression $cmd

            # Wait until the process finishes before continuing.

            while ((Get-Process | Where { $_.Name -eq 'wusa'}) -ne $null) { 
                Start-Sleep -Seconds 1
            }
        }

        Pop-Location
        Write-Output "Finished installing update packages on the device."
    }
    else {
        Write-Output "Skipping update packages because path was not found."
    }
}

function Create-SharedUser {

    # Provision a shared local account based on the device's name.

    param (
        [Parameter(Mandatory=$true, HelpMessage = "Password to use for the device's shared user account")][string] $Password
    )

    $LocalUserName = $env:ComputerName -replace "-", ""
    Write-Output "Creating the local user account $LocalUserName."

    # Use NET USER to add the shared account to this device. This script
    # disables password expiration for the shared user account, which is
    # necessary in shared-device scenarios. However, this will break
    # password management in Mobile Device Management.

    net user $LocalUserName $Password /add /expires:never /passwordchg:no | Tee-Object -Variable Results | Out-Null
    if ($LASTEXITCODE -ne 0) {
        throw $Results
    }

    Write-Output "Configuring device to automatically sign in as $LocalUserName."
    Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" -Name DefaultDomainName -Value $env:ComputerName | Out-Null
    Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" -Name DefaultUserName   -Value $LocalUserName    | Out-Null
    New-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" -Name DefaultPassword   -Value $Password         | Out-Null
    Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" -Name AutoAdminLogon    -Value "1"               | Out-Null

    Write-Output "Finished creating the local user account on the device."
}

function Apply-WirelessProfiles {

    # Import each wireless profile found in the configuration store.

    param (
        [Parameter(Mandatory=$true, HelpMessage = "Path of the folder containing wireless profiles to add to the device")][string] $Path,
        [Parameter(Mandatory=$true, HelpMessage = "Name of the interface with which to associate the wireless profiles")][string] $Interface
    )

    if ((Test-Path -Path $Path -PathType Container)) {
        Push-Location -Path $Path
    
        $Profiles = Get-ChildItem -Filter *.xml
    
        $ProfilesCount = ($Profiles | Measure-Object).Count
        Write-Output "Importing ($ProfilesCount) wireless profiles from the configuration store."

        $Profiles | ForEach-Object {
            Write-Output "...$_"

            netsh.exe wlan add profile filename=$_ interface=$Interface | Tee-Object -Variable Results | Out-Null
            if ($LASTEXITCODE -ne 0) {
                throw $Results
            }
        }

        Pop-Location
        Write-Output "Finished importing wireless profiles from the configuration store."
    }
    else {
        Write-Output "Skipping wireless profiles because path was not found."
    }
}

# MAIN

Write-Output "Beginning to configure this device for shared use."
Write-Output "------------------------------------------------------------------------"
Apply-WirelessProfiles $ProfilesPath $Interface
Write-Output "------------------------------------------------------------------------"
Apply-LocalFiles $FilesPath $FilesTarget
Write-Output "------------------------------------------------------------------------"
Apply-RegFiles $SettingsPath
Write-Output "------------------------------------------------------------------------"
Apply-GroupPolicy $PoliciesPath
Write-Output "------------------------------------------------------------------------"
Import-ScheduledTasks $TasksPath $TaskUser $TaskPassword
Write-Output "------------------------------------------------------------------------"
Apply-UpdateFiles $UpdatesPath
Write-Output "------------------------------------------------------------------------"
Create-SharedUser $UserPassword
Write-Output "------------------------------------------------------------------------"
Apply-AppxPackages $AppsPath
Write-Output "------------------------------------------------------------------------"
Log-DeviceWithMac $LogsPath
Write-Output "------------------------------------------------------------------------"
Write-Output "Finished configuring this device for shared use."

Preparing personal devices for delivery

When you prepare personal devices for delivery, you use the same configuration store you used for shared devices. That includes copying the local GPO and wireless networking profiles from a reference device to the configuration store, adding APPX and MSU files, and so on.

After you have stocked the configuration store, preparing personal devices for delivery can be easier and a bit quicker than preparing shared devices, mainly because you do not have to complete the OOBE when configuring the device. Instead, you start devices in Audit mode. In Audit mode, you can configure and customize devices prior to delivering them to students. After the first time students start their devices, they see the OOBE. For more information about Audit mode, see Audit Mode Overview.

To prepare personal devices for delivery, complete the following steps:

  1. Start the device, and wait for the OOBE to begin.

  2. Tap the Accessibility icon, tap On Screen Keyboard, and then press Ctrl+Shift+Fn+F3 to start the device in Audit mode, signing in to the local Administrator automatically.

  3. At an elevated command prompt, run the code in Listing 19, which then launches the code in Listing 20.

    Because Sysprep cannot finish while restarts are pending, Listing 20 finishes by copying Exit-AuditMode.ps1 (Listing 21) and Unattend.xml (Listing 22) to the device and configures the device so that it runs Exit-AuditMode.ps1 the next time the device starts. Exit-AuditMode.ps1 runs Sysprep to reseal the device, exiting Audit mode and shutting the device down.

  4. Deliver the device to the student.

Listing 19. Apply-PersonalConfig.cmd

@echo off

rem Apply-PersonalConfig.cmd
rem
rem Start Apply-PersonalConfig.ps1, bypassing execution policy.

powershell.exe -ExecutionPolicy Bypass %~dp0Apply-PersonalConfig.ps1 -StorePath D:\Store

Listing 20. Apply-PersonalConfig.ps1

# Apply-PersonalConfig.ps1
# Apply settings from the configuration store to the local device,
# and prepare the device for delivery to the student by running Sysprep.
# This script configures personal devices. See Apply-SharedConfig.ps1
# for a script that prepares devices for shared scenarios. See the guide
# "Deploying Windows RT 8.1 in education" for more information about using
# and customizing this script to configure Windows RT devices in schools.
# DISCLAIMER
# ----------
# This sample script is not supported under any Microsoft standard support program
# or service. This sample script is provided AS IS without warranty of any kind.
# Microsoft further disclaims all implied warranties including, without limitation,
# any implied warranties of merchantability or of fitness for a particular purpose.
# The entire risk arising out of the use or performance of this sample script and
# documentation remains with you. In no event shall Microsoft, its authors, or anyone
# else involved in the creation, production, or delivery of this sample script be
# liable for any damages whatsoever (including, without limitation, damages for loss
# of business profits, business interruption, loss of business information, or other
# pecuniary loss) arising out of the use of or inability to use this sample script or
# documentation, even if Microsoft has been advised of the possibility of such damages.

param (
    [Parameter(Mandatory=$true, HelpMessage = "Path to the folder containing the configuration store")][string] $StorePath
)

$ErrorActionPreference = 'Stop'
if (!(Test-Path -Path $StorePath -PathType Container)) {
    throw "$StorePath was not found."
}

# GLOBAL VARIABLES ##############################################

$ScriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path

# The following variables define subfolder names within
# the configuration store. These scripts expect specific types
# of files to appear in specific subfolders. If you change the
# following folder and file names, you must also change them in
# Gather-DeviceConfig.pst, Apply-PersonalConfig.pst, and
# Update-DeviceConfig.pst.

$AppsPath        = Join-Path $StorePath "Apps"
$FilesPath       = Join-Path $StorePath "Files"
$LogsPath        = Join-Path $StorePath "Logs"
$PoliciesPath    = Join-Path $StorePath "Policies"
$ProfilesPath    = Join-Path $StorePath "Profiles"
$SettingsPath    = Join-Path $StorePath "Settings"
$TasksPath       = Join-Path $StorePath "Tasks"
$UpdatesPath     = Join-Path $StorePath "Updates"

# The following variables define the user name and password
# to use to create scheduled tasks on each device. The scripts
# will add this account to the local device and use it when
# creating each scheduled task.

$TaskUser        = "DevAdmin"
$TaskPassword    = "Passw0rd"

# Additional variables:

$Interface       = "Wi-Fi"       # The name of the Wi-Fi interface on Surface devices
$FilesTarget     = "C:"          # The root of the file system for applying local files.

# FUNCTIONS #####################################################

function Apply-AppxPackages {

    # Install each Windows Store app from the configuration store.
    # Make sure the Group Policy setting AllowAllTrustedApps is enabled
    # and a sideloading product key is installed on the device.

    param (
        [Parameter(Mandatory=$true, HelpMessage = "Path of the folder containing Windows Store app (APPX) packages")][string] $Path
    )

    if ((Test-Path -Path $Path -PathType Container)) {

        Push-Location -Path $Path
    
        $AppPackages = Get-ChildItem -Filter *.appx
    
        $PackageCount = ($AppPackages | Measure-Object).Count
        Write-Output "Installing ($PackageCount) apps from the configuration store."

        $AppPackages | ForEach-Object {
            Write-Output "...$_"
            Add-AppxProvisionedPackage -Online -PackagePath $_ -SkipLicense
        }

        Pop-Location
        Write-Output "Finished installing app packages on the device."
    }
    else {
        Write-Output "Skipping Windows Store apps because path was not found."
    }
}

function Apply-LocalFiles {

    # Copy files and folders from the configuration store.

    param (
        [Parameter(Mandatory=$true, HelpMessage = "Path of the folder containing folders and files to copy")][string] $Path,
        [Parameter(Mandatory=$true, HelpMessage = "Target path to which to copy the source folders and files")][string] $Target
    )

    if ((Test-Path -Path $Path -PathType Container)) {
        Write-Output "Applying files and folders to this device."
        xcopy.exe $Path\*.* $Target\*.* /s /d /e /h /r /k /y | Tee-Object -Variable Results | Out-Null
        if ($LASTEXITCODE -ne 0) {
            throw $Results
        }
        Write-Output "Finished applying files and folders to this device."
    }
    else {
        Write-Output "Skipping local files because path was not found."
    }
}

Function Log-DeviceWithMac {

    # Create a file containing the computer name, MAC address
    # of the first Wi-Fi adapter, and the device's serial number.

    param (
        [Parameter(Mandatory=$true, HelpMessage = "Path in which to log the computer's name, MAC address, and serial number")][string] $Path
    )

    if ((Test-Path -Path $Path -PathType Container)) {
        Write-Output "Logging the computer name and MAC address in the configuration store."

        $FileName = $env:ComputerName + "_" + $((Get-NetAdapter -Name "Wi-FI").MacAddress ) + ".txt"
        $FullFilePath = Join-Path $Path $FileName
    
        # Check if the file exists do not write a new file, otherwise write the file.

        If (!(Test-Path $FullFilePath)) {
            $Content = $env:ComputerName + ", " + $((Get-NetAdapter -Name "Wi-FI").MacAddress ) + ", " + (Get-WmiObject -Class Win32_BIOS).SerialNumber
            Add-Content -Path $FullFilePath -Value $Content
            Write-Output "...$Content"
        }
        Write-Output "Finished logging the computer name and MAC address in the configuration store."
    }
    else {
        Write-Output "Did not log the device because path was not found."
    }
}

function Enable-GroupPolicy {

    # Enable and start the Group Policy service.

    Set-Service -Name gpsvc -StartupType auto
    Start-Service -Name gpsvc
}

function Apply-GroupPolicy {

    # Apply local Group Policy settings from A configuration store
    # to the local computer, and start the Group Policy service.

    param (
        [Parameter(Mandatory=$true, HelpMessage = "Path of the folder containing the local Group Policy object to copy")][string] $Path
    )
    $Target          = "C:\Windows\System32\GroupPolicy"
    $SecurityInfPath = Join-Path $Target "security.inf"
    $SecuritySdbPath = Join-Path $Target "secedit.sdb"

    if ((Test-Path -Path $Path -PathType Container)) {
        Write-Output "Configuring Group Policy on this device."

        Write-Output "...Copying policy settings to the device."
        xcopy $Path\*.* $Target\*.* /s /d /h /r /y | Tee-Object -Variable Results | Out-Null
        if ($LASTEXITCODE -ne 0) {
            throw $Results
        }

        # For personal configurations, this script does not import the
        # security settings by using secedit. These security settings can
        # interfere with running Sysprep in Audit mode, because they
        # disable the local Administrator account by default. To work
        # around this problem, the script Exit-AuditMode.ps1 imports the
        # security settings just prior to running Sysprep.

        # Write-Output "...Configuring security policy on the device."
        # secedit /configure /db $SecuritySdbPath /cfg $SecurityInfPath | Out-Null

        Write-Output "...Enabling and starting the Group Policy service."
        Enable-GroupPolicy

        Write-Output "...Updating Group Policy on the device."
        gpupdate /force | Tee-Object -Variable Results | Out-Null
        if ($LASTEXITCODE -ne 0) {
            throw $Results
        }

        Write-Output "Finished configuring Group Policy on the device."
    }
    else {
        Write-Output "Skipping Group Policy because path was not found."
    }
}

function Apply-RegFiles {

    # Import each registry file found in the configuration store.

    param (
        [Parameter(Mandatory=$true, HelpMessage = "Path of the folder containing registry (REG) files to import on the device")][string] $Path
    )

    if ((Test-Path -Path $Path -PathType Container)) {
        Push-Location -Path $Path
    
        $RegFiles = Get-ChildItem -Filter *.reg
    
        $RegFileCount = ($RegFiles | Measure-Object).Count
        Write-Output "Importing ($RegFileCount) REG files from the configuration store."

        $RegFiles | ForEach-Object {
            Write-Output "...$_"
            reg import $_ | Tee-Object -Variable Results | Out-Null
            if ($LASTEXITCODE -ne 0) {
                throw $Results
            }
        }

        Pop-Location
        Write-Output "Finished importing REG files from the configuration store."
    }
    else {
        Write-Output "Skipping settings because path was not found."
    }
}

function Import-ScheduledTasks {

    # Install each task found in the configuration store.

    param (
        [Parameter(Mandatory=$true, HelpMessage = "Path of the folder containing task (XML) files to import into scheduled tasks")][string] $Path,
        [Parameter(Mandatory=$true, HelpMessage = "Name of the account under which to run each imported scheduled task")][string] $TaskUser,
        [Parameter(Mandatory=$true, HelpMessage = "Password for the account under which to run each imported scheduled task")][string] $TaskPassword
    )

    if ((Test-Path -Path $Path -PathType Container)) {
        Push-Location -Path $Path
    
        # Create the local administrator account to use for running the tasks.

        Write-Output "Creating the local administrator account for $TaskUser."
        net user $TaskUser $TaskPassword /add /expires:never /passwordchg:no | Tee-Object -Variable Results | Out-Null
        if ($LASTEXITCODE -ne 0) {
            throw $Results
        }
        net localgroup "Administrators" $TaskUser /add | Tee-Object -Variable Results | Out-Null
        if ($LASTEXITCODE -ne 0) {
            throw $Results
        }

        # Add each task file to the task scheduler, using our local administrator account.

        $TaskFiles = Get-ChildItem -Filter *.xml
    
        $TaskFileCount = ($TaskFiles | Measure-Object).Count
        Write-Output "Importing ($TaskFileCount) scheduled tasks from the configuration store."

        $TaskFiles | ForEach-Object {
            Write-Output "...$_"
            $TaskXML = get-content $_ | Out-String
            Register-ScheduledTask -Xml $TaskXML -TaskName $_ -User $TaskUser -Password $TaskPassword
        }

        Pop-Location
        Write-Output "Finished importing scheduled tasks from the configuration store."
    }
    else {
        Write-Output "Skipping tasks because path was not found."
    }
}

function Apply-UpdateFiles {

    # Install each update found in the configuration store. Windows RT does
    # not support WSUS and an update catalog is not available. Contact your
    # account team about acquiring update packages (MSU files).

    param (
        [Parameter(Mandatory=$true, HelpMessage = "Folder containing Microsoft update (MSU) files to install on the device")][string] $Path
    )

    if ((Test-Path -Path $Path -PathType Container)) {
        Push-Location -Path $Path
    
        $UpdatePackages = Get-ChildItem -Filter *.msu
    
        $PackageCount = ($UpdatePackages | Measure-Object).Count
        Write-Output "Installing ($PackageCount) updates from the configuration store."

        $UpdatePackages | ForEach-Object {
            Write-Output "...$_"
            $cmd = "wusa $_ /quiet /norestart"
            Invoke-Expression $cmd

            # Wait until the process finishes before continuing.

            while ((Get-Process | Where { $_.Name -eq 'wusa'}) -ne $null) { 
                Start-Sleep -Seconds 1
            }
        }

        Pop-Location
        Write-Output "Finished installing update packages on the device."
    }
    else {
        Write-Output "Skipping update packages because path was not found."
    }
}

function Apply-WirelessProfiles {

    # Import each wireless profile found in the configuration store.

    param (
        [Parameter(Mandatory=$true, HelpMessage = "Path of the folder containing wireless profiles to add to the device")][string] $Path,
        [Parameter(Mandatory=$true, HelpMessage = "Name of the interface with which to associate the wireless profiles")][string] $Interface
    )

    if ((Test-Path -Path $Path -PathType Container)) {
        Push-Location -Path $Path
    
        $Profiles = Get-ChildItem -Filter *.xml
    
        $ProfilesCount = ($Profiles | Measure-Object).Count
        Write-Output "Importing ($ProfilesCount) wireless profiles from the configuration store."

        $Profiles | ForEach-Object {
            Write-Output "...$_"

            netsh.exe wlan add profile filename=$_ interface=$Interface | Tee-Object -Variable Results | Out-Null
            if ($LASTEXITCODE -ne 0) {
                throw $Results
            }
        }

        Pop-Location
        Write-Output "Finished importing wireless profiles from the configuration store."
    }
    else {
        Write-Output "Skipping wireless profiles because path was not found."
    }
}

function Start-Sysprep {

    # Prepare the device to run Sysprep the next time the device starts.

    param (
        [Parameter(Mandatory=$true, HelpMessage = "Path of the folder containing this script and Unattend.xml")][string] $ScriptPath
    )

    $TargetPath = "C:\Windows\Panther"
    $SaveUnattendFile = "Unattend.sav"
    $RunOnceSource = Join-Path $ScriptPath Exit-AuditMode.ps1
    $RunOnceTarget = Join-Path $TargetPath Exit-AuditMode.ps1
    $SourceUnattendFile = Join-Path $ScriptPath Unattend.xml
    $TargetUnattendFile = Join-Path $TargetPath Unattend.xml

    # Copy the Unattend.xml file from the configuration store
    # to the Panther folder on the Windows RT device.

    if (Test-Path $SourceUnattendFile ) {
        Write-Output "Copying Unattend.xml to the device."
        if (Test-Path $TargetUnattendFile ) {
            Rename-Item $TargetUnattendFile $SaveUnattendFile
        }
        Copy-Item $SourceUnattendFile $TargetUnattendFile
    }

    # Prepare the Windows RT device to run Exit-AuditMode.ps1 the
    # next time the device starts. We use the RunOnce registry key.

    if (test-path $RunOnceSource ) {
        Write-Output "Preparing the device to restart."
        Copy-Item $RunOnceSource $RunOnceTarget
        New-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce -Name Sysprep -Value "powershell.exe -ExecutionPolicy Bypass $RunOnceTarget" | Out-Null
    }
}

# MAIN ##########################################################

Write-Output "Beginning to configure this device for personal use."
Write-Output "This process is complete after the device shuts down."
Write-Output "------------------------------------------------------------------------"
Apply-WirelessProfiles $ProfilesPath $Interface
Write-Output "------------------------------------------------------------------------"
Apply-LocalFiles $FilesPath $FilesTarget
Write-Output "------------------------------------------------------------------------"
Apply-RegFiles $SettingsPath
Write-Output "------------------------------------------------------------------------"
Apply-GroupPolicy $PoliciesPath
Write-Output "------------------------------------------------------------------------"
Import-ScheduledTasks $TasksPath $TaskUser $TaskPassword
Write-Output "------------------------------------------------------------------------"
Apply-UpdateFiles $UpdatesPath
Write-Output "------------------------------------------------------------------------"
Apply-AppxPackages $AppsPath
Write-Output "------------------------------------------------------------------------"
Log-DeviceWithMac $LogsPath
Write-Output "------------------------------------------------------------------------"
Start-Sysprep $ScriptPath
shutdown /r /t 0

Listing 21. Exit-AuditMode.ps1

# Exit-AuditMode.ps1
# The script Apply-PersonalConfig.ps1 uses this script to exit Audit mode by running Sysprep.
# Sysprep cannot run while restarts are pending. Apply-PersonalConfig sets this script to run
# the next time the device starts, and then restarts the device to clear pending restarts.
# See the guide "Deploying Windows RT 8.1 in education" for more information about using
# and customizing this script to configure Windows RT devices in schools.
# DISCLAIMER
# ----------
# This sample script is not supported under any Microsoft standard support program
# or service. This sample script is provided AS IS without warranty of any kind.
# Microsoft further disclaims all implied warranties including, without limitation,
# any implied warranties of merchantability or of fitness for a particular purpose.
# The entire risk arising out of the use or performance of this sample script and
# documentation remains with you. In no event shall Microsoft, its authors, or anyone
# else involved in the creation, production, or delivery of this sample script be
# liable for any damages whatsoever (including, without limitation, damages for loss
# of business profits, business interruption, loss of business information, or other
# pecuniary loss) arising out of the use of or inability to use this sample script or
# documentation, even if Microsoft has been advised of the possibility of such damages.

$Target          = "C:\Windows\System32\GroupPolicy"
$SecurityInfPath = Join-Path $Target "security.inf"
$SecuritySdbPath = Join-Path $Target "secedit.sdb"

# Run secedit to import the security configuration. This must be done
# just prior to running Sysprep because the security configuration can
# interfere with restarting the device in Audit mode, since the default
# security configuration disables the local Administrator account.

secedit /configure /db $SecuritySdbPath /cfg $SecurityInfPath | Out-Null
Stop-Process -Name sysprep -ErrorAction SilentlyContinue
C:\Windows\System32\Sysprep\Sysprep.exe /oobe /shutdown

Listing 22. Unattend.xml

<?xml version="1.0" encoding="utf-8"?>
<unattend xmlns="urn:schemas-microsoft-com:unattend">
    <settings pass="specialize">
        <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="arm" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="https://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <ComputerName>Surface</ComputerName>
            <RegisteredOrganization>Contoso</RegisteredOrganization>
            <RegisteredOwner>Contoso</RegisteredOwner>
            <DoNotCleanTaskBar>true</DoNotCleanTaskBar>
            <TimeZone>Pacific Standard Time</TimeZone>
        </component>
        <component name="Microsoft-Windows-International-Core" processorArchitecture="arm" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="https://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <InputLocale>0409:00000409</InputLocale>
            <SystemLocale>en-US</SystemLocale>
            <UILanguage>en-US</UILanguage>
            <UserLocale>en-US</UserLocale>
        </component>
        <component name="Security-Malware-Windows-Defender" processorArchitecture="arm" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="https://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <TrustedImageIdentifier>81b553fb-1321-4862-9269-594e5c04c1f2</TrustedImageIdentifier>
        </component>
    </settings>
    <settings pass="oobeSystem">
        <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="arm" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="https://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <OEMInformation>
                <HelpCustomized>false</HelpCustomized>
                <Logo>C:\Windows\System32\OOBE\Info\surface.bmp</Logo>
                <Manufacturer>Microsoft Corporation</Manufacturer>
                <SupportURL>https://www.microsoft.com/surface/support</SupportURL>
                <SupportPhone>U.S./Canada: 1-800-Microsoft (642-7676); Mexico: 01 800 123 3353</SupportPhone>
            </OEMInformation>
            <OOBE>
                <HideEULAPage>true</HideEULAPage>
                <HideLocalAccountScreen>false</HideLocalAccountScreen>
                <HideOnlineAccountScreens>false</HideOnlineAccountScreens>
                <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
                <NetworkLocation>Work</NetworkLocation>
                <ProtectYourPC>1</ProtectYourPC>
                <HideOEMRegistrationScreen>true</HideOEMRegistrationScreen>
            </OOBE>
            <StartTiles>
                <WideTiles>
                    <WideTile1>
                        <AppId>Microsoft.FreshPaint_8wekyb3d8bbwe!Microsoft.FreshPaint</AppId>
                    </WideTile1>
                </WideTiles>
                <SquareTiles>
                    <SquareTile1>
                        <AppId>Microsoft.Office.OneNote_8wekyb3d8bbwe!microsoft.onenoteim</AppId>
                    </SquareTile1>
                    <SquareTile2>
                        <AppId>Microsoft.SkypeWiFi_kzf8qxf38zg5c!App</AppId>
                    </SquareTile2>
                </SquareTiles>
            </StartTiles>
            <UserAccounts>
                <LocalAccounts>
                    <LocalAccount wcm:action="add">
                        <Password>
                            <Value>Passw0rd</Value>
                            <PlainText>true</PlainText>
                        </Password>
                        <Description></Description>
                        <DisplayName>SchoolAdmin</DisplayName>
                        <Name>SchoolAdmin</Name>
                        <Group>Administrators</Group>
                    </LocalAccount>
                </LocalAccounts>
            </UserAccounts>
            <RegisteredOrganization>Contoso</RegisteredOrganization>
            <RegisteredOwner>Contoso</RegisteredOwner>
            <TimeZone>Pacific Standard Time</TimeZone>
        </component>
        <component name="Microsoft-Windows-International-Core" processorArchitecture="arm" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="https://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <InputLocale>0409:00000409</InputLocale>
            <SystemLocale>en-US</SystemLocale>
            <UILanguage>en-US</UILanguage>
            <UserLocale>en-US</UserLocale>
        </component>
    </settings>
</unattend>

See also