Search This Blog

Tuesday 25 September 2018

Batch - Automate building Windows PE and optional components with a script

Install this language pack, then that...

Why?

Windows PE is invaluable for system maintenance and recovery. The process of creating bootable media is well documented by Microsoft. During this process, you have to add language packs and optional components if you wish for their functionality to be accessible in the final boot environment. Then you have to add the language packs to the image for the corresponding optional components for all installed language packs. Then you set the image language.
My bolding shows my point of disapproval... This is unacceptably long and complicated with a lot of boiler plate commands. All I wanted was two WinPE ISOs, one for amd64 and one for x86 with most optional components in EN-GB.

The process goes...

1: Copy the required architecture of PE from the ADK install location to a new place.
2: Mount the image into the already created mounted folder
3 (optional): set scratch space
4: Install the language pack you require
5: Install an optional component.
6: Install language pack for that component if they exist.
7: Repeat 6 for all installed languages
8: Repeat 5, 6 and 7 for all required components.
9: Set default language
10: Unmount the image
11: Create ISO from folder structure

Build it quick!

Put this in a text file, save it with a .bat extension and run as administrator to make both amd64 and x86 flavours of Windows PE (tested with WinPE 10) in folders 'WinPE10amd64' and 'WinPE10x86' on the system drive root with most features enabled and default language set to en-gb. It also creates a log file, opens it at the end and runs a PowerShell command to highlight any failed operations.

@echo off
call:IsAdmin
call "%SystemDrive%\Program Files (x86)\Windows Kits\10\Assessment and Deployment Kit\Deployment Tools\DandISetEnv.bat"
call:CreatePE x86
call:CreatePE amd64

pause
goto:eof

:CreatePE
set OUTPE=%SystemDrive%\WinPE10%1

echo Copying Windows 10 PE %1 Files ...
call copype %1 "%OUTPE%" > "%SystemDrive%\PE%1_log.txt"

echo - Mounting image
Dism /Mount-Image /ImageFile:"%OUTPE%\media\sources\boot.wim" /index:1 /MountDir:"%OUTPE%\mount" >> "%SystemDrive%\PE%1_log.txt"

echo - Setting scratch space capacity
Dism /Set-ScratchSpace:512 /Image:"%OUTPE%\mount" >> "%SystemDrive%\PE%1_log.txt"

echo - Adding Language Packages
call:AddLP %1 en-us
call:AddLP %1 en-gb

echo - Applying Optional Components
call:AddOC %1 WinPE-MDAC
call:AddOC %1 WinPE-FMAPI
call:AddOC %1 WinPE-EnhancedStorage
call:AddOC %1 WinPE-WDS-Tools
call:AddOC %1 WinPE-RNDIS
call:AddOC %1 WinPE-Dot3Svc
call:AddOC %1 WinPE-GamingPeripherals
call:AddOC %1 WinPE-WinReCfg
call:AddOC %1 WinPE-WMI
call:AddOC %1 WinPE-SecureStartup
call:AddOC %1 WinPE-NetFX
call:AddOC %1 WinPE-Scripting
call:AddOC %1 WinPE-PowerShell
call:AddOC %1 WinPE-DismCmdlets
call:AddOC %1 WinPE-SecureBootCmdlets
call:AddOC %1 WinPE-StorageWMI
call:AddOC %1 WinPE-PlatformID

echo - Setting Language
Dism /Set-AllIntl:en-GB /Image:"%OUTPE%\mount" >> "%SystemDrive%\PE%1_log.txt"

echo - Committing changes
Dism /Unmount-Image /MountDir:"%OUTPE%\mount" /commit >> "%SystemDrive%\PE%1_log.txt"

echo Creating ISO ...
call MakeWinPEMedia /ISO "%OUTPE%" "%OUTPE%.iso"

start notepad "%SystemDrive%\PE%1_log.txt"
start powershell -EP bypass -NoE -Command "Select-String -Path '%SystemDrive%\PE%1_log.txt' -Pattern 'operation'"

echo Process complete.
goto:eof

:AddOC
echo -- Applying %2
Dism /Add-Package /Image:"%SystemDrive%\WinPE10%1\mount" /PackagePath:"C:\Program Files (x86)\Windows Kits\10\Assessment and Deployment Kit\Windows Preinstallation Environment\%1\WinPE_OCs\%2.cab" >> "%SystemDrive%\PE%1_log.txt"
set l=en-us
if exist "C:\Program Files (x86)\Windows Kits\10\Assessment and Deployment Kit\Windows Preinstallation Environment\%1\WinPE_OCs\%l%\%2_%l%.cab" (
echo --- Applying language pack for %l%
Dism /Add-Package /Image:"%SystemDrive%\WinPE10%1\mount" /PackagePath:"C:\Program Files (x86)\Windows Kits\10\Assessment and Deployment Kit\Windows Preinstallation Environment\%1\WinPE_OCs\%l%\%2_%l%.cab" >> "%SystemDrive%\PE%1_log.txt"
)
set l=en-gb
if exist "C:\Program Files (x86)\Windows Kits\10\Assessment and Deployment Kit\Windows Preinstallation Environment\%1\WinPE_OCs\%l%\%2_%l%.cab" (
echo --- Applying language pack for %l%
Dism /Add-Package /Image:"%SystemDrive%\WinPE10%1\mount" /PackagePath:"C:\Program Files (x86)\Windows Kits\10\Assessment and Deployment Kit\Windows Preinstallation Environment\%1\WinPE_OCs\%l%\%2_%l%.cab" >> "%SystemDrive%\PE%1_log.txt"
)
goto:eof

:AddLP
echo -- Applying language pack for %2
Dism /Add-Package /Image:"%SystemDrive%\WinPE10%1\mount" /PackagePath:"C:\Program Files (x86)\Windows Kits\10\Assessment and Deployment Kit\Windows Preinstallation Environment\%1\WinPE_OCs\%2\lp.cab" >> "%SystemDrive%\PE%1_log.txt"
goto:eof

:IsAdmin
%systemroot%\system32\Reg.exe query "HKU\S-1-5-19\Environment"
If Not %ERRORLEVEL% EQU 0 (
Cls & Echo You must have administrator rights to continue ...
Pause & Exit
)
Cls
goto:eof

Powershell - Reading INI files into PSObjects for easy handling

"But I want it that way..."

Why INI?

I've embraced the .NET reliance in PowerShell, regex handling, direct member calls and dot notation. I really like using PowerShell (and worth the price - ISESteroids) to develop scripts for managing the 800+ computers I maintain. From bulk domain management to full system deployments with faster results than MDT, PowerShell can handle it all. If you're working with deploying applications and creating deployments, handling INI files becomes inevitable and it's worth reading the fairly simple structure of an INI file being that they are so often used in installers, setup files and others.  Other files, such as INF, take the INI structure further and provide more complex functionality, but there are many old and new technologies still using INIs as configuration files and some of the  Microsoft Windows system files are in INI format.

My Interest in this

Handling files with a text editor is faster when working with one or two files, but if you need to handle hundreds of copies distributed across computers or folders, then this becomes tedious. I'm a huge fan and advocate of PortableApps.com for having a suite of tools on a USB stick. The folder structure for this comprises of a few INI files and I wanted a quick way to reference this information in a script so I could remove all the games.

There are already ways

In the search to find a Google solution, I discovered this Microsoft Scripting Guy post that describes how to feed a file, line by line through PowerShell's switch function with the -File parameter and provides links to more complete scripts which, with much respect to Oliver Lipkau, gave me the base idea and regex strings.

Tweak, tweak...

A few things were lacking in the solution that I really wanted to change:

 - The returned object is a HashTable
I embrace the PowerShell over .Net so a raw hash table although may be faster than other methods, can be more cumbersome to handle in code with try catch statements and reading the object in the console requires additional discovery before querying values. I also think this looks ugly and doesn't leverage the dot notation or autocomplete of PowerShell.

 - The returned values are out of order when exported
The sections and values of the INI are loaded into a HashTable which does not retain structure order when writing back to the file (given the functions supplied). This can be particularly irritating for manual editors who like to keep the order of comments, sections and values.

So I rewrote the PowerShell Functions for a little more flavour.
Results were:
 - PowerShell 2.0 support
 - Verbose is noisy, SilentlyContinue is quiet

For the Import:
 - Accepts path input from pipeline (absolute paths)
- Full Line Comments are noted in a ";Comment#" NoteProperty
- Inline Comments are noted in a ";InlineComment#" NoteProperty preceding the property or section
- Errors and warnings based on $ErrorActionPreference
- The Object returned is a PSCustomObject with dot notation accessible settable properties.
- Quotation translation (" and ') is performed.
- Multi line (line ending in ' \') is supported by switch

For the Export:
- Values, Sections and Comments are to be written back into the file in the same order. 
- Confirmation will be shown on attempted overwrite, but not on append, unless forced.
- Encoding option can be selected from default set.
- Switches to enclose the section names, property names and values in quotaion marks

How to use:

Import-INIToPSObject:
$example = Import-INIToPSObject -Path C:\Temp\test.ini
Then let PowerShell do the autocomplete magic:


Listing a section:


Modifying Values:

Export-PSObjectToINI:
$example | Export-PSObjectToINI -Path C:\Temp\test.ini

Sometimes you're modifying, sometimes you're creating files on the fly. If the latter is the case, you can create a new INI Object by defining a structure like this:
[pscustomobject]@{
  rootProp1='test1'
  rootProp2=$null
  section1=[pscustomobject]@{
    sectProp1=$true
    sectProp2=4
  }
  section2=[pscustomobject]@{
    sectProp1=$false
    sectProp2=''
  }
}|Export-PSObjectToINI -Path 'C:\temp\test.ini'

Not as pretty but... CODE!

The code is now available on the PowerShell Gallery!

Wednesday 6 June 2018

PowerShell Tip - Did the script run as an administrator (Elevated)?

No frills...

Nice and simple one liner for top of scripts to quickly stop 'forgot to elevate' mistakes on quick solutions:
if(-not((New-Object Security.Principal.WindowsPrincipal $([Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator))){Write-Host 'Failed to run as Administrator.';Pause;Exit}

Friday 1 December 2017

PowerShell Golden Egg - Delete files/folders on Reboot the Microsoft Way (MoveFileEx)

Yeah, just do it on reboot...

So simple, eh? Can't delete the script while it's running. but once the script reboots the PC, there's just that to delete and you're done. So delete it on the reboot yeah? If only it was that simple...

Run\RunOnce

So Microsoft have these "run and runonce" reg keys, one set for each user, one set for the machine itself. But as people have discovered, the time at which these keys execute is between OS starting to load and when the user logon is initiated, but this is before the desktop has loaded, which can complicate things and this has been depreciated in Windows 10 in favour of Asynchronous commands. Also, through my own experiences, I can attest to the complications of having to set a run\runonce command in script which requires some interesting syntax skill and it's worth nothing that the current user run\runonce keys run under the credentials of THAT user. So if they aren't an administrator, the command will be run in a standard user context with the restrictions that apply. All in all, I found this approach buggy, prone to many unusual errors dependent on OS or machine and insufferably complex for something as simple as deleting on reboot.

How do Microsoft do it?

The PendingFileRenameOperations key is a red herring.

Installers, updates and all kinds of packages delete files on reboot and they aren't leveraging keys like this, so what's the correct approach? Well, according to Microsoft documentation, the information for these file changes on reboot is kept in the  'HKLM\SYSTEM\CurrentControlSet\Control\Session Manager' key under the value 'PendingFileRenameOperations' which is a 'REG_MULTI_SZ' or MultiString in PowerShell. Now this seems like a good idea, and in a basic format of source and destination, but you soon find that the strings in the multistring array are null terminated, meaning, they need to have this at end of the string for the values to be read correctly and missing these or messing them up can corrupt the key, causing the computer to constantly think there's pending changes. So trying to keep the null values intact while parsing the key through a PowerShell script seems daunting. Instead...

MoveFileEx!!!


Microsoft have their own Windows API function (or method, my jargon is flexible) for handling this key in the correct manner, so if we declare a new type with the call to that definition, we can use the function to have Windows delete the file on reboot correctly. This poor guy on reddit was so close and I give him many thanks for basically handing me most of the code but the thread is locked and I can't seem to contact the guy. Still, for anyone reading this...

THE ANSWER!!!

Null... ish... (That reddit guy's answer)

PowerShell has a few ideas of what 'null' actually is. Is it an object with a type, but no value; an object without type; the absence of an object? Well, the one that answered the question for me was '[Management.Automation.Language.NullString]::Value()'. This is important because the destination of the MoveFileEx function has to be inputted as the correct type of null for the command to succeed which is what that guy needed.

Add-Type but no Remove-Type

It's also worth noting that I made some use of try catch to check for type loading. This is because the type is loaded in the AppDomain context and can't be unloaded unless a new session is established. The default namespace for these autogenerated types is 'Microsoft.PowerShell.Commands.AddType.AutoGeneratedTypes'.

The Code

function Move-OnReboot([parameter(Mandatory=$True)]$Path, $Destination){
Begin{
try{
[Microsoft.PowerShell.Commands.AddType.AutoGeneratedTypes.MoveFileUtils]|Out-Null
}catch{
$memberDefinition = @’
[DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Auto)]
public static extern bool MoveFileEx(string lpExistingFileName, string lpNewFileName, int dwFlags);
‘@
Add-Type -Name MoveFileUtils -MemberDefinition $memberDefinition
}
}
Process{
$Path="$((Resolve-Path $Path).Path)"
if ($Destination){
$Destination = $executionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Destination)
}else{$Destination = [Management.Automation.Language.NullString]::Value}
$MOVEFILE_DELAY_UNTIL_REBOOT = 0x00000004
[Microsoft.PowerShell.Commands.AddType.AutoGeneratedTypes.MoveFileUtils]::MoveFileEx($Path, $Destination, $MOVEFILE_DELAY_UNTIL_REBOOT)
}
End{}
}


Stick this in a script, module, whatever.
Pass it a path.
Pass it a destination to move to another path or omit to delete the original on reboot.
Get a boolean success or fail on return.

Files First - Bonus Material

One joyful quirk of the MoveFileEx operation is that although you can set a folder to delete on reboot, it will not actually perform the command if the folder contains files. This means you must set the files within the folder to delete before you set the folder to delete.

Bonus, bonus...

My script is contained in one folder when deployed, so I made a simple compression of the function code specific for deleting the script folder and containing files only, no sub folders. Placed in a script, will schedule the parent folder and all files for deletion. Put the header at the top of your script and the footer just before the last reboot call.

# Header
$scriptPath=$MyInvocation.MyCommand.Definition
$scriptFolder=(Split-Path -Parent $scriptPath)
function D-OR([parameter(Mandatory=$True)]$Path){
$Path="$((Resolve-Path $Path).Path)"
try{
[Microsoft.PowerShell.Commands.AddType.AutoGeneratedTypes.MoveFileUtils]|Out-Null
}catch{
$memberDefinition = @’
[DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Auto)]
public static extern bool MoveFileEx(string lpExistingFileName, string lpNewFileName, int dwFlags);
‘@
Add-Type -Name MoveFileUtils -MemberDefinition $memberDefinition
}
"Attempting to delete $Path on Reboot"
[Microsoft.PowerShell.Commands.AddType.AutoGeneratedTypes.MoveFileUtils]::MoveFileEx($Path,[Management.Automation.Language.NullString]::Value,4)
}
# Footer
if(Test-Path $scriptFolder){
 Get-ChildItem $scriptFolder|Select-Object -ExpandProperty FullName|ForEach-Object{D-OR $_}
 D-OR $scriptFolder
}

Sunday 26 November 2017

PowerShell Tip - Compare Secure Strings

No frills...

So the PC is freshly imaged, but needs connecting to the domain using a script. No available way to authenticate the password before making the method call and fat fingers can screw up the join. For a little extra assurance, enter password twice and compare, but SecureStrings are not comparable, so:

Function Password-DoubleCheck{
  do{
    $pass1=Read-Host -Prompt 'Enter User Password' -AsSecureString
    $pass2=Read-Host -Prompt 'Verfy User Password' -AsSecureString
    $chk1 = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($pass1))
    $chk2 = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($pass2))
    if($chk1-ceq$chk2){$validPass=$True}else{$validPass=$False;Write-Host 'Passwords do not match...'}
  }
  until($validPass)
  $pass1
}

Use like:
$password=(Password-DoubleCheck)

Derived from: http://techibee.com/powershell/compare-secure-strings-entered-through-powershell/422

Wednesday 22 November 2017

PowerShell Tip - Determine if the OS 64 bit in a consistent way

No frills...

Just wanted a consistent way of determining if the operating system was 32 or 64 bit regardless of PowerShell version or OS.

Function OSIs-64Bit{if([IntPtr]::Size-eq 4){$false}else{$true}}

Paste atop a script and use in an if statement like:
if(OSIs-64Bit){'Do some wicked stuff'}

Tuesday 13 June 2017

Minecraft Launcher Error 5 Fix

Script for performing Mojang 'Temporary Solution'

Wifey hit with known unfixed bug

So that's it. There's a bug in the latest iterations of the Minecraft Native Launcher for Windows that goes like this (https://bugs.mojang.com/browse/MCL-5291) :
When the launcher needs to update, it downloads a copy to "%MinecraftInstallDir%\tmp\tmpLauncher.tmp". This is actually the new version of MinecraftLauncher.exe.
It then attempts to replace the current launcher at "\%MinecraftInstallDir%MinecraftLauncher.exe" with the downloaded temp file.
This is when users are seeing an 'Error 5' (Which, to me is 'Access Denied', but may be completely unrelated).

A lot of sites suggest that you should delete the temp file and try again, which some users have reported success with but what I'm reading in the updateLog.txt doesn't seem to match. The one solution that I find definitely works is to manually copy over the file as suggested in the "temporary" solution from Mojang.

This issue has reoccurred nearly every update and the bug doesn't seem to be going away, so..,

No Darling, I said 'Batch... it...'

Copy and paste this into notepad and save.
Rename .bat instead of .txt. 
When the error displays, close and 'Run as administrator' on batch file.

@echo off
:: Admin Test
"%systemroot%\system32\reg.exe" query "HKU\S-1-5-19\Environment" > NUL
If Not %ERRORLEVEL% EQU 0 (
Cls & Echo You must have administrator rights to continue.
Echo Please close and restart this batch file by
Echo right-clicking and select 'Run as administrator'.
Pause & Exit
)

:: Set Paths
if exist "%PROGRAMFILES(x86)%" (
Set MCLPath="%PROGRAMFILES(x86)%\Minecraft\MinecraftLauncher.exe"
Set MCTPath="%PROGRAMFILES(x86)%\Minecraft\tmp\tmpLauncher.tmp"
) else (
Set MCLPath="%PROGRAMFILES%\Minecraft\MinecraftLauncher.exe"
Set MCTPath="%PROGRAMFILES%\Minecraft\tmp\tmpLauncher.tmp"
)

::Check files exist
if NOT exist %MCLPath% ( echo Minecraft.exe not found at %MCLPath% & pause & exit )
if NOT exist %MCTPath% ( echo tmpLauncher.tmp not found at %MCTPath% & pause & exit )

:Copy and overwrite
copy /y %MCTPath% %MCLPath%

:Launch
%MCLPath%

exit