Powershell script to install font family - windows

Below is my script istalling Monserrat fonts from zip file. I can't figure how to check if a font already installed. After installation I can open folder C:\Windows\Fonts\Montserrat and I see al of them. When I am running script second time, it is not recognize existance of this folder. Where is my mistake?
$Source = "Montserrat.zip"
$FontsFolder = "FontMontserrat"
Expand-Archive $Source -DestinationPath $FontsFolder
$FONTS = 0x14
$CopyOptions = 4 + 16;
$objShell = New-Object -ComObject Shell.Application
$objFolder = $objShell.Namespace($FONTS)
$allFonts = dir $FontsFolder
foreach($File in $allFonts)
{
If((Test-Path "C:\Windows\Fonts\Montserrat") -eq $True)
{
echo "Font $File already installed"
}
Else
{
echo "Installing $File"
$CopyFlag = [String]::Format("{0:x}", $CopyOptions);
$objFolder.CopyHere($File.fullname,$CopyFlag)
}
}

Finally my script:
$Source = "Montserrat.zip"
$FontsFolder = "FontMontserrat"
Expand-Archive $Source -DestinationPath $FontsFolder -Force
$FONTS = 0x14
$CopyOptions = 4 + 16;
$objShell = New-Object -ComObject Shell.Application
$objFolder = $objShell.Namespace($FONTS)
$allFonts = dir $FontsFolder
foreach($font in Get-ChildItem -Path $fontsFolder -File)
{
$dest = "C:\Windows\Fonts\$font"
If(Test-Path -Path $dest)
{
echo "Font $font already installed"
}
Else
{
echo "Installing $font"
$CopyFlag = [String]::Format("{0:x}", $CopyOptions);
$objFolder.CopyHere($font.fullname,$CopyFlag)
}
}
I am running this script by following cmd:
set batchPath=%~dp0
powershell.exe -noexit -file "%batchPath%InstMontserrat.ps1"
I don't have to run it as administrator, but user have admin permissions.

Corrections of your script based on my comment assuming Windows 10:
# well-known SID for admin group
if ('S-1-5-32-544' -notin [System.Security.Principal.WindowsIdentity]::GetCurrent().Groups) {
throw 'Script must run as admin!'
}
$source = 'Montserrat.zip'
$fontsFolder = 'FontMontserrat'
Expand-Archive -Path $source -DestinationPath $fontsFolder
foreach ($font in Get-ChildItem -Path $fontsFolder -File) {
$dest = "C:\Windows\Fonts\$font"
if (Test-Path -Path $dest) {
"Font $font already installed."
}
else {
$font | Copy-Item -Destination $dest
}
}

If you do not want to install the font on OS level but only make it available for programs to use until reboot you may want to use this script that:
Will fail/throw if it cannot register/unregister font.
Broadcasts WM_FONTCHANGE to inform all windows that fonts have changed
Does not require administrator privileges
Does not install fonts in Windows, only makes them available for all programs in current session until reboot
Has verbose mode for debugging
Does not work with font folders
Usage:
register-fonts.ps1 [-v] [-unregister <PATH>[,<PATH>...]] [-register <PATH>[,<PATH>...]] # Register and unregister at same time
register-fonts.ps1 [-v] -unregister <PATH>
register-fonts.ps1 [-v] -register <PATH>
register-fonts.ps1 [-v] <PATH> # Will register font path
Param (
[Parameter(Mandatory=$False)]
[String[]]$register,
[Parameter(Mandatory=$False)]
[String[]]$unregister
)
# Stop script if command fails https://stackoverflow.com/questions/9948517/how-to-stop-a-powershell-script-on-the-first-error
$ErrorActionPreference = "Stop"
add-type -name Session -namespace "" -member #"
[DllImport("gdi32.dll")]
public static extern bool AddFontResource(string filePath);
[DllImport("gdi32.dll")]
public static extern bool RemoveFontResource(string filePath);
[return: MarshalAs(UnmanagedType.Bool)]
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
public static extern bool PostMessage(IntPtr hWnd, int Msg, int wParam = 0, int lParam = 0);
"#
$broadcast = $False;
Foreach ($unregisterFontPath in $unregister) {
Write-Verbose "Unregistering font $unregisterFontPath"
# https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-removefontresourcea
$success = [Session]::RemoveFontResource($unregisterFontPath)
if (!$success) {
Throw "Cannot unregister font $unregisterFontPath"
}
$broadcast = $True
}
Foreach ($registerFontPath in $register) {
Write-Verbose "Registering font $registerFontPath"
# https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-addfontresourcea
$success = [Session]::AddFontResource($registerFontPath)
if (!$success) {
Throw "Cannot register font $registerFontPath"
}
$broadcast = $True
}
if ($broadcast) {
# HWND_BROADCAST https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-postmessagea
$HWND_BROADCAST = New-Object IntPtr 0xffff
# WM_FONTCHANGE https://learn.microsoft.com/en-us/windows/win32/gdi/wm-fontchange
$WM_FONTCHANGE = 0x1D
Write-Verbose "Broadcasting font change"
# Broadcast will let other programs know that fonts were changed https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-postmessagea
$success = [Session]::PostMessage($HWND_BROADCAST, $WM_FONTCHANGE)
if (!$success) {
Throw "Cannot broadcase font change"
}
}
The script was inspired by this gist https://gist.github.com/Jaykul/d53a16ce5e7d50b13530acb4f98aaabd

Related

How to Change the My Computer Desktop Icon Using Powershell?

I am trying to learn a little bit of Shell-Scripting, as Test-Automation seems to get trendier these days.
My grasp of Powershell is quite generic. My current goal is to change the My Computer desktop icon.
There are some things of the Microsoft Windows 10 operating system that I have not touched yet using Powershell. I hope that maybe some more prolific writers than myself are able to give me a hand in reaching this goal.
I have just tested two snippets that have surprisingly run successfully on their first attempts.
The first one may be called as Create_Shortcut.PS1 and creates a desktop icon for the command-line preprocessing system where batch files may be run.
# Creates the command-line desktop icon.
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
$TargetFile = "C:\Windows\System32\Cmd.Exe"
$ShortcutFile = "\\ISXPFV01.hd00.example.com\us_qv2_dem_user_data_pool_nra$\EE65037.HD00\Desktop\Command-Line.Lnk"
$WScriptShell = New-Object -COMObject WScript.Shell
$Shortcut = $WScriptShell.CreateShortcut($ShortcutFile)
$Shortcut.TargetPath = $TargetFile
$Shortcut.Save()
The second one might be called as Rename_My_Computer.PS1 and it renames the My Computer desktop icon.
# Changes the My Computer desktop icon name from "This PC" to "VSDC0365".
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
$My_Computer = 17
$Shell = New-Object -COMObject Shell.Application
$NSComputer = $Shell.Namespace($My_Computer)
$NSComputer.Self.Name = $Env:COMPUTERNAME
What I am interested in could prove to be extremely simple to someone who is more experienced in Powershell than myself. I need to change the My Computer desktop icon by specifying its path.
As I have not reached yet this goal, any kind of help on this subject is highly appreciated.
Thanks for reading.
UPDATE after #Theo's great comment:
A new surprisingly working snippet, that manages to produce the My Computer desktop icon:
# HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\HideDesktopIcons\NewStartPanel\
# {20D04FE0-3AEA-1069-A2D8-08002B30309D}
# 0 = show
# 1 = hide
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
$Path = "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\HideDesktopIcons\NewStartPanel"
$Name = "{20D04FE0-3AEA-1069-A2D8-08002B30309D}"
$Exist = "Get-ItemProperty -Path $Path -Name $Name"
if ($Exist)
{
Set-ItemProperty -Path $Path -Name $Name -Value 0
}
Else
{
New-ItemProperty -Path $Path -Name $Name -Value 0
}
Now, all that I have to do is to somehow programatically press F5 in order to refresh the Desktop View, after somehow setting the settings that he has mentioned in his comment.
Another UPDATE related to the refresh:
Another surprisingly working snippet, that refreshes the Desktop View:
# Refresh Desktop Ability
$Definition = #'
[System.Runtime.InteropServices.DllImport("Shell32.dll")]
private static extern int SHChangeNotify(int eventId, int flags, IntPtr item1, IntPtr item2);
public static void Refresh() {
SHChangeNotify(0x8000000, 0x1000, IntPtr.Zero, IntPtr.Zero);
}
'#
Add-Type -MemberDefinition $Definition -Namespace WinAPI -Name Explorer
# Refresh desktop icons
[WinAPI.Explorer]::Refresh()
Now everything that is still left is to change the My Computer desktop icon somehow before the refresh.
UPDATE related to Taking the Ownership of that Registry Key:
Tricky stuff. I had no idea that it can get so complicated.
Currently, it fails with error messages starting with the following one:
PS Y:\> Y:\Digitization\Powershell\The_My_Computer_Desktop_Icon\Change_Registry_Key.PS1
True
Exception calling "OpenSubKey" with "3" argument(s): "Requested registry access is not allowed."
At Y:\Digitization\Powershell\The_My_Computer_Desktop_Icon\Change_Registry_Key.PS1:139 char:1
+ $RegKey = [Microsoft.Win32.Registry]::ClassesRoot.OpenSubKey( ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : SecurityException
This is the content of the Change_Registry_Key.PS1 file:
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
Function Enable-Privilege {
Param(
## The privilege to adjust.
[ValidateSet(
"SeAssignPrimaryTokenPrivilege"
, "SeAuditPrivilege"
, "SeBackupPrivilege"
, "SeChangeNotifyPrivilege"
, "SeCreateGlobalPrivilege"
, "SeCreatePagefilePrivilege"
, "SeCreatePermanentPrivilege"
, "SeCreateSymbolicLinkPrivilege"
, "SeCreateTokenPrivilege"
, "SeDebugPrivilege"
, "SeEnableDelegationPrivilege"
, "SeImpersonatePrivilege"
, "SeIncreaseBasePriorityPrivilege"
, "SeIncreaseQuotaPrivilege"
, "SeIncreaseWorkingSetPrivilege"
, "SeLoadDriverPrivilege"
, "SeLockMemoryPrivilege"
, "SeMachineAccountPrivilege"
, "SeManageVolumePrivilege"
, "SeProfileSingleProcessPrivilege"
, "SeRelabelPrivilege"
, "SeRemoteShutdownPrivilege"
, "SeRestorePrivilege"
, "SeSecurityPrivilege"
, "SeShutdownPrivilege"
, "SeSyncAgentPrivilege"
, "SeSystemEnvironmentPrivilege"
, "SeSystemProfilePrivilege"
, "SeSystemtimePrivilege"
, "SeTakeOwnershipPrivilege"
, "SeTcbPrivilege"
, "SeTimeZonePrivilege"
, "SeTrustedCredManAccessPrivilege"
, "SeUndockPrivilege"
, "SeUnsolicitedInputPrivilege")]
$Privilege
## The process on which to adjust the privilege. Defaults to the current process.
, $ProcessId = $Pid
## Switch to disable the privilege, rather than enable it.
, [Switch] $Disable
)
$Definition = #'
using System;
using System.Runtime.InteropServices;
public class AdjPriv
{
[DllImport( "advapi32.dll"
, ExactSpelling = true
, SetLastError = true)]
internal static extern bool AdjustTokenPrivileges( IntPtr htok
, bool disall
, ref TokPriv1Luid newst
, int len
, IntPtr prev
, IntPtr relen);
[DllImport( "advapi32.dll"
, ExactSpelling = true
, SetLastError = true)]
internal static extern bool OpenProcessToken( IntPtr h
, int acc
, ref IntPtr phtok);
[DllImport( "advapi32.dll"
, SetLastError = true)]
internal static extern bool LookupPrivilegeValue( string host
, string name
, ref long pluid);
[StructLayout( LayoutKind.Sequential
, Pack = 1)]
internal struct TokPriv1Luid
{
public int Count;
public long Luid;
public int Attr;
}
internal const int SE_PRIVILEGE_ENABLED = 0x00000002;
internal const int SE_PRIVILEGE_DISABLED = 0x00000000;
internal const int TOKEN_QUERY = 0x00000008;
internal const int TOKEN_ADJUST_PRIVILEGES = 0x00000020;
public static bool EnablePrivilege( long processHandle
, string privilege
, bool disable)
{
bool retVal;
TokPriv1Luid tp;
IntPtr hproc = new IntPtr(processHandle);
IntPtr htok = IntPtr.Zero;
retVal = OpenProcessToken( hproc
, TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY
, ref htok);
tp.Count = 1;
tp.Luid = 0;
if(disable)
{
tp.Attr = SE_PRIVILEGE_DISABLED;
}
else
{
tp.Attr = SE_PRIVILEGE_ENABLED;
}
retVal = LookupPrivilegeValue( null
, privilege
, ref tp.Luid);
retVal = AdjustTokenPrivileges( htok
, false
, ref tp
, 0
, IntPtr.Zero
, IntPtr.Zero);
return retVal;
}
}
'#
$ProcessHandle = (Get-Process -Id $ProcessId).Handle
$Type = Add-Type $Definition -PassThru
$Type[0]::EnablePrivilege($processHandle, $Privilege, $Disable)
}
Enable-Privilege SeTakeOwnershipPrivilege
# Change Owner to the local Administrators group.
$RegKey = [Microsoft.Win32.Registry]::ClassesRoot.OpenSubKey( `
"CLSID\{20D04FE0-3AEA-1069-A2D8-08002B30309D}" `
, [Microsoft.Win32.RegistryKeyPermissionCheck]::ReadWriteSubTree `
, [System.Security.AccessControl.RegistryRights]::TakeOwnership)
$RegACL = $RegKey.GetAccessControl()
$RegACL.SetOwner([System.Security.Principal.NTAccount]"Administrators")
$RegKey.SetAccessControl($RegACL)
# Change Permissions for the local Administrators group.
$RegKey = [Microsoft.Win32.Registry]::ClassesRoot.OpenSubKey( `
"CLSID\{20D04FE0-3AEA-1069-A2D8-08002B30309D}" `
, [Microsoft.Win32.RegistryKeyPermissionCheck]::ReadWriteSubTree `
, [System.Security.AccessControl.RegistryRights]::ChangePermissions)
$RegACL = $RegKey.GetAccessControl()
$RegRule = New-Object System.Security.AccessControl.RegistryAccessRule( `
"Administrators" `
, "FullControl" `
, "ContainerInherit" `
, "None" `
, "Allow")
$RegACL.SetAccessRule($RegRule)
$RegKey.SetAccessControl($RegACL)
UPDATE related to Another Attempt of Taking the Ownership of that Registry Key:
This is the content of another snippet, called as Change_Registry_Key.2.PS1:
#Define HKCR
New-PSDrive -Name HKCR7 `
-PSProvider Registry `
-Root HKEY_CLASSES_ROOT
#Set $Path HKCR Key Path
$Path = "HKCR:\CLSID\{20D04FE0-3AEA-1069-A2D8-08002B30309D}"
#Set $Path Permissions
$ACL = Get-ACL $Path
$Rule = New-Object System.Security.AccessControl.RegistryAccessRule ( `
"<domain>\<username>" `
, "FullControl" `
, "Allow")
$ACL.SetAccessRule($Rule)
$ACL | Set-ACL -Path $path
#Set HKCR 'Attributes' Key Value
Set-ItemProperty -Path $Path `
-Name Attributes `
-Value b0940064
These are the errors that appear in the console area:
PS Y:\> Y:\Digitization\Powershell\The_My_Computer_Desktop_Icon\Change_Registry_Key.2.PS1
Name Used (GB) Free (GB) Provider Root
---- --------- --------- -------- ----
HKCR7 Registry HKEY_CLASSES_ROOT
Exception calling "SetAccessRule" with "1" argument(s): "Some or all identity refere
nces could not be translated."
At Y:\Digitization\Powershell\The_My_Computer_Desktop_Icon\Change_Registry_Key.2.PS1
:17 char:1
+ $ACL.SetAccessRule($Rule)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : IdentityNotMappedException
Set-ACL : Requested registry access is not allowed.
At Y:\Digitization\Powershell\The_My_Computer_Desktop_Icon\Change_Registry_Key.2.PS1
:19 char:8
+ $ACL | Set-ACL -Path $path
+ ~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : PermissionDenied: (HKEY_CLASSES_RO...8-08002B30309D}:
String) [Set-Acl], SecurityException
+ FullyQualifiedErrorId : System.Security.SecurityException,Microsoft.PowerShel
l.Commands.SetAclCommand
Set-ItemProperty : Requested registry access is not allowed.
At Y:\Digitization\Powershell\The_My_Computer_Desktop_Icon\Change_Registry_Key.2.PS1
:22 char:1
+ Set-ItemProperty -Path $Path `
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : PermissionDenied: (HKEY_CLASSES_RO...8-08002B30309D}:
String) [Set-ItemProperty], SecurityException
+ FullyQualifiedErrorId : System.Security.SecurityException,Microsoft.PowerShel
l.Commands.SetItemPropertyCommand
UPDATE Regarding #Theo's Second Version of Renaming the My Computer Desktop Icon:
This version is not working for me yet.
Its testing is quite simple:
I am manually renaming the My Computer Desktop Icon to Fifi;
then I am running this snippet;
then I am manually refreshing the Desktop View.
Although I expect the My Computer Desktop Icon to be renamed back to Work-Laptop, its name still remains fixed as Fifi.
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
#Requires -RunAsAdministrator
Function Enable-Privilege {
[CmdletBinding( ConfirmImpact = 'low'
, SupportsShouldProcess = $false)]
[OutputType('System.Boolean')]
Param(
[Parameter( Mandatory = $true
, Position = 0)]
[ValidateSet( "SeAssignPrimaryTokenPrivilege"
, "SeAuditPrivilege"
, "SeBackupPrivilege"
, "SeChangeNotifyPrivilege"
, "SeCreateGlobalPrivilege"
, "SeCreatePagefilePrivilege"
, "SeCreatePermanentPrivilege"
, "SeCreateSymbolicLinkPrivilege"
, "SeCreateTokenPrivilege"
, "SeDebugPrivilege"
, "SeEnableDelegationPrivilege"
, "SeImpersonatePrivilege"
, "SeIncreaseBasePriorityPrivilege"
, "SeIncreaseQuotaPrivilege"
, "SeIncreaseWorkingSetPrivilege"
, "SeLoadDriverPrivilege"
, "SeLockMemoryPrivilege"
, "SeMachineAccountPrivilege"
, "SeManageVolumePrivilege"
, "SeProfileSingleProcessPrivilege"
, "SeRelabelPrivilege"
, "SeRemoteShutdownPrivilege"
, "SeRestorePrivilege"
, "SeSecurityPrivilege"
, "SeShutdownPrivilege"
, "SeSyncAgentPrivilege"
, "SeSystemEnvironmentPrivilege"
, "SeSystemProfilePrivilege"
, "SeSystemtimePrivilege"
, "SeTakeOwnershipPrivilege"
, "SeTcbPrivilege"
, "SeTimeZonePrivilege"
, "SeTrustedCredManAccessPrivilege"
, "SeUndockPrivilege"
, "SeUnsolicitedInputPrivilege")]
[String]$Privilege
, [Parameter(Position = 1)]
$ProcessId = $PID
, [switch]$Disable
)
Add-Type -TypeDefinition #'
using System;
using System.Runtime.InteropServices;
public class Privilege {
[DllImport( "advapi32.dll"
, ExactSpelling = true
, SetLastError = true)]
internal static extern bool AdjustTokenPrivileges(
IntPtr htok
, bool disall
, ref TokPriv1Luid newst
, int len
, IntPtr prev
, IntPtr relen);
[DllImport( "advapi32.dll"
, ExactSpelling = true
, SetLastError = true)]
internal static extern bool OpenProcessToken( IntPtr h
, int acc
, ref IntPtr phtok);
[DllImport( "advapi32.dll"
, SetLastError = true)]
internal static extern bool LookupPrivilegeValue( string host
, string name
, ref long pluid);
[StructLayout( LayoutKind.Sequential
, Pack = 1)]
internal struct TokPriv1Luid {
public int Count;
public long Luid;
public int Attr;
}
internal const int SE_PRIVILEGE_ENABLED = 0x00000002;
internal const int SE_PRIVILEGE_DISABLED = 0x00000000;
internal const int TOKEN_QUERY = 0x00000008;
internal const int TOKEN_ADJUST_PRIVILEGES = 0x00000020;
public static bool EnablePrivilege( long processHandle
, string privilege
, bool disable) {
bool retVal;
TokPriv1Luid tp;
IntPtr hproc = new IntPtr(processHandle);
IntPtr htok = IntPtr.Zero;
retVal = OpenProcessToken( hproc
, TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY
, ref htok);
tp.Count = 1;
tp.Luid = 0;
if(disable) {
tp.Attr = SE_PRIVILEGE_DISABLED;
}
else {
tp.Attr = SE_PRIVILEGE_ENABLED;
}
retVal = LookupPrivilegeValue( null
, privilege
, ref tp.Luid);
retVal = AdjustTokenPrivileges( htok
, false
, ref tp
, 0
, IntPtr.Zero
, IntPtr.Zero);
return retVal;
}
}
'#
try {
$proc = Get-Process -Id $ProcessId -ErrorAction Stop
$name = $proc.ProcessName
$handle = $proc.Handle
$action = if ($Disable) { 'Disabling' } else { 'Enabling' }
Write-Verbose ( "{0} privilege '{1}' for process {2}" -f $action `
, $Privilege `
, $name)
[Privilege]::EnablePrivilege( $handle `
, $Privilege `
, [bool]$Disable)
}
catch {
throw
}
}
################################################################
# Step 1: Give the current process the SeTakeOwnershipPrivilege.
################################################################
$null = Enable-Privilege -Privilege SeTakeOwnershipPrivilege -Verbose
##############################################################
# Step 2: change Owner to the local Administrators group
##############################################################
# Better not use the string "Administrators", because this
# might have a different name in other cultures.
#
# $RegACL.SetOwner([System.Security.Principal.NTAccount]"Administrators")
#
# Use the Well-Known SID instead.
# Local Administrators Group.
$Administrators = `
[System.Security.Principal.SecurityIdentifier]::new('S-1-5-32-544')
$RegKey = [Microsoft.Win32.Registry]::ClassesRoot.OpenSubKey( `
"CLSID\{20D04FE0-3AEA-1069-A2D8-08002B30309D}" `
, [Microsoft.Win32.RegistryKeyPermissionCheck]::ReadWriteSubTree `
, [System.Security.AccessControl.RegistryRights]::TakeOwnership)
$RegACL = $RegKey.GetAccessControl()
$RegACL.SetOwner($Administrators)
$RegKey.SetAccessControl($RegACL)
##############################################################
# Step 3: Give the Local Administrators Group Full Control.
##############################################################
# Refresh the A.C.L.
$RegACL = $RegKey.GetAccessControl()
# Test if there is a Deny rule in the ACL
# for Administrators and, if so, remove that rule.
$RegACL.GetAccessRules( `
$true `
, $true `
, [System.Security.Principal.SecurityIdentifier]) | `
Where-Object { `
$_.AccessControlType -eq 'Deny' `
-and $_.IdentityReference -eq $Administrators.Value `
} | `
ForEach-Object { $null = $RegAcl.RemoveAccessRule($_) }
# Create a new rule allowing the Administrators Full Control.
$RegRule = [System.Security.AccessControl.RegistryAccessRule]::new( `
$Administrators `
, 'FullControl' `
, 'ContainerInherit' `
, 'None' `
, 'Allow')
$RegACL.SetAccessRule($RegRule)
$RegKey.SetAccessControl($RegACL)
# Close the Registry Key.
$RegKey.Close()
##############################################################
# Step 4: Change the 'LocalizedString' property
# in the registry to suit your needs.
##############################################################
#
# With PowerShell 5, you need to use
# `Registry::HKEY_CLASSES_ROOT\..` syntax in order to be able
# to set the registry Type for the value
# with parameter '-Type'.
# As of PowerShell 7, the '-Type' parameter is included.
$RegPath = `
'Registry::HKEY_CLASSES_ROOT\CLSID\{20D04FE0-3AEA-1069-A2D8-08002B30309D}'
Set-ItemProperty -Path $RegPath `
-Name 'LocalizedString' `
-Value "%ComputerName%" `
-Type ExpandString `
-Force
What I get in the console is the following text:
PS C:\WINDOWS\system32> C:\Users\MihaiDobrescu\OneDrive\Documents\2_Facturi\12_-_Bank_Services\Digitization\Powershell\Change_Registry_Key.3.PS1 -RunAsAdministrator
VERBOSE: Enabling privilege 'SeTakeOwnershipPrivilege' for process powershell_ise
PS C:\WINDOWS\system32>
UPDATE: A Final Version, with all of the Ingredients of the Beautiful Soup inside of it.
Thanks go again to #Theo who has actually debugged the whole mess.
Note: The snippet is surprisingly working even inside the Virtual Machine, as there is no need for it to be run as an Administrator, since no ownership has to be taken upon the whole Solar System in order to solve this problem.
# How to test:
#
# 1. Rename the My Computer Desktop Icon to "Fifi".
# 2. Remove the My Computer Desktop Icon from the Desktop View.
# 3. Run this snippet.
# 4. Observe how the My Computer Desktop Icon is produced on the Desktop View,
# with the name "Tele-Ordinator" and with a very emotional Desktop Icon.
# Allow the execution of snippets.
Set-ExecutionPolicy `
-ExecutionPolicy RemoteSigned `
-Scope CurrentUser
# Produce the My Computer Desktop Icon on the Desktop View.
# HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\HideDesktopIcons\NewStartPanel\
# {20D04FE0-3AEA-1069-A2D8-08002B30309D}
# 0 = show
# 1 = hide
$Path = "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\HideDesktopIcons\NewStartPanel"
$Name = "{20D04FE0-3AEA-1069-A2D8-08002B30309D}"
$Exist = "Get-ItemProperty -Path $Path -Name $Name"
if ($Exist)
{
Set-ItemProperty `
-Path $Path `
-Name $Name `
-Value 0
}
Else
{
New-ItemProperty `
-Path $Path `
-Name $Name `
-Value 0
}
# Rename the My Computer Desktop Icon from "This PC" to "Tele-Ordinator".
$My_Computer = 17
$Shell = New-Object -COMObject Shell.Application
$NSComputer = $Shell.Namespace($My_Computer)
$NSComputer.Self.Name = "Tele-Ordinator"
# Change the My Computer Desktop Icon.
$RegPath = `
'Registry::HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\CLSID\{20D04FE0-3AEA-1069-A2D8-08002B30309D}\DefaultIcon'
if (!(Test-Path -Path $RegPath))
{
$null = New-Item `
-Path $RegPath `
-Force
}
Set-ItemProperty `
-Path $RegPath `
-Name '(Default)' `
-Value 'Y:\Digitization\Icons\Robsonbillponte-Happy-Holidays-Pictures.ICO' `
-Type ExpandString `
-Force
# Refresh the Desktop View.
$Definition = #'
[System.Runtime.InteropServices.DllImport("Shell32.dll")]
private static extern int SHChangeNotify(
int eventId
, int flags
, IntPtr item1
, IntPtr item2);
public static void Refresh()
{
SHChangeNotify(
0x8000000
, 0x1000
, IntPtr.Zero
, IntPtr.Zero);
}
'#
Add-Type `
-MemberDefinition $Definition `
-Namespace WinAPI `
-Name Explorer
[WinAPI.Explorer]::Refresh()
Another Final UPDATE for setting and running a batch file that automates the above automation using the Microsoft Windows Batch Files Preprocessing System:
This is just a short snippet called as Change_Desktop_Icons.BAT.
ChDir %SystemRoot%\System32\WindowsPowerShell\v1.0\
%SystemRoot%\System32\WindowsPowerShell\v1.0\PowerShell.Exe Y:\Digitization\PowerShell\The_My_Computer_Desktop_Icon\Change_Desktop_Icon.PS1
Pause
This is the output of the thing, upon the double-click of its own Desktop Icon.
'\\ISXPFV01.hd00.example.com\us_qv2_dem_user_data_pool_nra$\EE65037.HD00\Desktop'
CMD.EXE was started with the above path as the current directory.
UNC paths are not supported. Defaulting to Windows directory.
C:\Windows>ChDir C:\WINDOWS\System32\WindowsPowerShell\v1.0\
C:\Windows\System32\WindowsPowerShell\v1.0>C:\WINDOWS\System32\WindowsPowerShell\v1.0\PowerShell.Exe Y:\Digitization\PowerShell\The_My_Computer_Desktop_Icon\Change_Desktop_Icon.PS1
C:\Windows\System32\WindowsPowerShell\v1.0>Pause
Press any key to continue . . .
As commented, it is quite a hassle to change the 'Computer' icons caption..
Below code works for me on Windows 10 Pro using PowerShell 5.1
you need run this as Administrator
#Requires -RunAsAdministrator
function Enable-Privilege {
[CmdletBinding(ConfirmImpact = 'low', SupportsShouldProcess = $false)]
[OutputType('System.Boolean')]
Param(
[Parameter(Mandatory = $true, Position = 0)]
[ValidateSet(
"SeAssignPrimaryTokenPrivilege", "SeAuditPrivilege", "SeBackupPrivilege", "SeChangeNotifyPrivilege",
"SeCreateGlobalPrivilege", "SeCreatePagefilePrivilege", "SeCreatePermanentPrivilege",
"SeCreateSymbolicLinkPrivilege", "SeCreateTokenPrivilege", "SeDebugPrivilege", "SeEnableDelegationPrivilege",
"SeImpersonatePrivilege", "SeIncreaseBasePriorityPrivilege", "SeIncreaseQuotaPrivilege",
"SeIncreaseWorkingSetPrivilege", "SeLoadDriverPrivilege", "SeLockMemoryPrivilege",
"SeMachineAccountPrivilege", "SeManageVolumePrivilege", "SeProfileSingleProcessPrivilege",
"SeRelabelPrivilege", "SeRemoteShutdownPrivilege", "SeRestorePrivilege", "SeSecurityPrivilege",
"SeShutdownPrivilege", "SeSyncAgentPrivilege", "SeSystemEnvironmentPrivilege", "SeSystemProfilePrivilege",
"SeSystemtimePrivilege", "SeTakeOwnershipPrivilege", "SeTcbPrivilege", "SeTimeZonePrivilege",
"SeTrustedCredManAccessPrivilege", "SeUndockPrivilege", "SeUnsolicitedInputPrivilege")]
[String]$Privilege,
[Parameter(Position = 1)]
$ProcessId = $PID,
[switch]$Disable
)
Add-Type -TypeDefinition #'
using System;
using System.Runtime.InteropServices;
public class Privilege {
[DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)]
internal static extern bool AdjustTokenPrivileges(IntPtr htok, bool disall, ref TokPriv1Luid newst, int len, IntPtr prev, IntPtr relen);
[DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)]
internal static extern bool OpenProcessToken(IntPtr h, int acc, ref IntPtr phtok);
[DllImport("advapi32.dll", SetLastError = true)]
internal static extern bool LookupPrivilegeValue(string host, string name, ref long pluid);
[StructLayout(LayoutKind.Sequential, Pack = 1)]
internal struct TokPriv1Luid {
public int Count;
public long Luid;
public int Attr;
}
internal const int SE_PRIVILEGE_ENABLED = 0x00000002;
internal const int SE_PRIVILEGE_DISABLED = 0x00000000;
internal const int TOKEN_QUERY = 0x00000008;
internal const int TOKEN_ADJUST_PRIVILEGES = 0x00000020;
public static bool EnablePrivilege(long processHandle, string privilege, bool disable) {
bool retVal;
TokPriv1Luid tp;
IntPtr hproc = new IntPtr(processHandle);
IntPtr htok = IntPtr.Zero;
retVal = OpenProcessToken(hproc, TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, ref htok);
tp.Count = 1;
tp.Luid = 0;
if(disable) { tp.Attr = SE_PRIVILEGE_DISABLED; }
else { tp.Attr = SE_PRIVILEGE_ENABLED; }
retVal = LookupPrivilegeValue(null, privilege, ref tp.Luid);
retVal = AdjustTokenPrivileges(htok, false, ref tp, 0, IntPtr.Zero, IntPtr.Zero);
return retVal;
}
}
'#
try {
$proc = Get-Process -Id $ProcessId -ErrorAction Stop
$name = $proc.ProcessName
$handle = $proc.Handle
$action = if ($Disable) { 'Disabling' } else { 'Enabling' }
Write-Verbose ("{0} privilege '{1}' for process {2}" -f $action, $Privilege, $name)
[Privilege]::EnablePrivilege($handle, $Privilege, [bool]$Disable)
}
catch {
throw
}
}
function Grant-FullControl ([string]$SubKey, [switch]$BreakInheritance) {
# helper function to grant registry FullControl for Administrators on a certain subkey
# better not use the string "Administrators", because this might have a different name in other cultures
# $RegACL.SetOwner([System.Security.Principal.NTAccount]"Administrators")
# use the Well-Known SID instead
$Administrators = [System.Security.Principal.SecurityIdentifier]::new('S-1-5-32-544') # local Administrators group
$RegKey = [Microsoft.Win32.Registry]::ClassesRoot.OpenSubKey($SubKey,
[Microsoft.Win32.RegistryKeyPermissionCheck]::ReadWriteSubTree,
[System.Security.AccessControl.RegistryRights]::TakeOwnership)
$RegACL = $RegKey.GetAccessControl()
if ($BreakInheritance) {
# break inheritance, but keep permissions
$RegACL.SetAccessRuleProtection($true, $true)
}
$RegACL.SetOwner($Administrators)
$RegKey.SetAccessControl($RegACL)
# refresh the ACL
$RegACL = $RegKey.GetAccessControl()
# test if there is a Deny rule in the ACL for Administrators and if so remove that rule
$RegACL.GetAccessRules($true, $true, [System.Security.Principal.SecurityIdentifier]) |
Where-Object { $_.AccessControlType -eq 'Deny' -and $_.IdentityReference -eq $Administrators.Value } |
ForEach-Object { $null = $RegAcl.RemoveAccessRule($_) }
# ceate a new rule allowing the Administrators FullControl
$RegRule =[System.Security.AccessControl.RegistryAccessRule]::new($Administrators,
'FullControl',
'ContainerInherit', # ContainerInherit, ObjectInherit
'None', # InheritOnly
'Allow')
$RegACL.SetAccessRule($RegRule)
$RegKey.SetAccessControl($RegACL)
# close the registry key
$RegKey.Close()
}
##################################################################################
# Step 1: give the current process the SeTakeOwnershipPrivilege
##################################################################################
$null = Enable-Privilege -Privilege SeTakeOwnershipPrivilege -Verbose
##################################################################################
# Step 2: change Key Owner to the local Administrators group and grant FullControl
##################################################################################
# first give Administrators full control on key "CLSID\{20D04FE0-3AEA-1069-A2D8-08002B30309D}"
Grant-FullControl -SubKey "CLSID\{20D04FE0-3AEA-1069-A2D8-08002B30309D}" -BreakInheritance
##################################################################################
# Step 3: change the 'LocalizedString' property in the registry to suit your needs
##################################################################################
# with PowerShell 5 you need to use `Registry::HKEY_CLASSES_ROOT\..` syntax in order to be able
# to set the registry Type for the value with parameter '-Type'.
# As of PowerShell 7 the '-Type' parameter is included
$regPath = 'Registry::HKEY_CLASSES_ROOT\CLSID\{20D04FE0-3AEA-1069-A2D8-08002B30309D}'
Set-ItemProperty -Path $regPath -Name 'LocalizedString' -Value "%ComputerName%" -Type ExpandString -Force
##################################################################################
# Step 4: OPTIONAL. Change the Computer icon for NEW user logins
##################################################################################
# give Administrators full control on subkey "CLSID\{20D04FE0-3AEA-1069-A2D8-08002B30309D}\DefaultIcon"
# now, we do not need to break the inheritance as we needed for the root key
Grant-FullControl -SubKey "CLSID\{20D04FE0-3AEA-1069-A2D8-08002B30309D}\DefaultIcon"
$regPath = 'Registry::HKEY_CLASSES_ROOT\CLSID\{20D04FE0-3AEA-1069-A2D8-08002B30309D}\DefaultIcon'
Set-ItemProperty -Path $regPath -Name '(Default)' -Value '%SystemRoot%\System32\imageres.dll,-149' -Type ExpandString -Force
##################################################################################
# Step 5: Change the Computer icon for the CURRENT user
##################################################################################
$regPath = 'Registry::HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\CLSID\{20D04FE0-3AEA-1069-A2D8-08002B30309D}\DefaultIcon'
Set-ItemProperty -Path $regPath -Name '(Default)' -Value '%SystemRoot%\System32\imageres.dll,-149' -Type ExpandString -Force
After all this, the desktop doesn't show the computername yet. You need to either press F5 on the desktop, or use the code you found to refresh the desktop.
To change the icon itself, you need to change the default value in registrypath
HKEY_CLASSES_ROOT\CLSID\{20D04FE0-3AEA-1069-A2D8-08002B30309D}\DefaultIcon
for future NEW logins, or registrypath
HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\CLSID\{20D04FE0-3AEA-1069-A2D8-08002B30309D}\DefaultIcon
for the CURRENT user.
By default, it points to %SystemRoot%\System32\imageres.dll,-109, which means it extracts icon no. 109 from the imageres.dll.
In that dll. there are ~343 icons, so you could opt to use another one from the same resource, or take one from another existing dll. (you can use IconsExtract from nirsoft for instance).
I didn't test this yet, but it should also be possible to have it point to the full path and filename of an icon of your own like %SystemDrive%\MyComputerIcon.ico.
As example, this will update the icon to use imageres.dll icon no. 149
$regPath = 'Registry::HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\CLSID\{20D04FE0-3AEA-1069-A2D8-08002B30309D}\DefaultIcon'
Set-ItemProperty -Path $regPath -Name '(Default)' -Value '%SystemRoot%\System32\imageres.dll,-149' -Type ExpandString -Force
which looks like this

Bring Powershell-Console to front from WinForms

im trying to bring my powershell console to front, even if it is minimized.
I found following code:
function Show-Process($Process, [Switch]$Maximize)
{
$sig = '
[DllImport("user32.dll")] public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);
[DllImport("user32.dll")] public static extern int SetForegroundWindow(IntPtr hwnd);
'
if ($Maximize) { $Mode = 3 } else { $Mode = 4 }
$type = Add-Type -MemberDefinition $sig -Name WindowAPI -PassThru
$hwnd = $process.MainWindowHandle
$null = $type::ShowWindowAsync($hwnd, $Mode)
$null = $type::SetForegroundWindow($hwnd)
}
Show-Process -Process (Get-Process -Id $pid)
It works fine, but when i call the function from a Button Click event, the console wont show.
What is the Problem? Is there a way to bring the powershell Console to front when using a WinForms GUI?
Here is the example GUI Code:
function Show-Process($Process, [Switch]$Maximize)
{
$sig = '
[DllImport("user32.dll")] public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);
[DllImport("user32.dll")] public static extern int SetForegroundWindow(IntPtr hwnd);
'
if ($Maximize) { $Mode = 3 } else { $Mode = 4 }
$type = Add-Type -MemberDefinition $sig -Name WindowAPI -PassThru
$hwnd = $process.MainWindowHandle
$null = $type::ShowWindowAsync($hwnd, $Mode)
$null = $type::SetForegroundWindow($hwnd)
}
Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.Application]::EnableVisualStyles()
$Form = New-Object system.Windows.Forms.Form
$Form.ClientSize = '446,266'
$Form.text = "Form"
$Form.TopMost = $false
$Button1 = New-Object system.Windows.Forms.Button
$Button1.text = "button"
$Button1.width = 60
$Button1.height = 30
$Button1.location = New-Object System.Drawing.Point(75,29)
$Button1.Font = 'Microsoft Sans Serif,10'
$Button1.Add_Click({
Show-Process -Process (Get-Process -Id $pid)
})
$Form.controls.AddRange(#($Button1))
[void]$Form.ShowDialog()
Thanks to #iRon's answer, i was able to figure it out, how i want it.
For anyone curious, the problem is, you only can get the consoles MainwindowHandle as long as ShowDialog wasn't called.
So i save the console Handle in a variable and i use the Form_Shown event to get the Form WindowHandle, since Form_Load still returns the Console Handle.
$sig = '
[DllImport("user32.dll")] public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);
[DllImport("user32.dll")] public static extern int SetForegroundWindow(IntPtr hwnd);'
$type = Add-Type -MemberDefinition $sig -Name WindowAPI -PassThru
[IntPtr]$handleConsole = (Get-Process -Id $pid).MainWindowHandle
[void]$type::ShowWindowAsync($handleConsole, 4);[void]$type::SetForegroundWindow($handleConsole)
Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.Application]::EnableVisualStyles()
$Form = New-Object system.Windows.Forms.Form
$Form.ClientSize = '446,266'
$Form.text = "Form"
$Form.TopMost = $false
$Form.Add_Shown({
$global:handleForm = (Get-Process -Id $pid).MainWindowHandle
})
$Button1 = New-Object system.Windows.Forms.Button
$Button1.text = "Clone ad-USer"
$Button1.width = 60
$Button1.height = 30
$Button1.location = New-Object System.Drawing.Point(75,29)
$Button1.Font = 'Microsoft Sans Serif,10'
$Button1.Add_Click({
[void]$type::ShowWindowAsync($handleConsole, 4);[void]$type::SetForegroundWindow($handleConsole)
Read-Host -Prompt "Please Enter a Value"
[void]$type::ShowWindowAsync($global:handleForm, 4);[void]$type::SetForegroundWindow($global:handleForm)
})
$Form.controls.AddRange(#($Button1))
[void]$Form.ShowDialog()
Now, if i press the Button, to console pops up in front.
After the User enter something into the Console, the Form comes to front again.
Unfortunately, I can't completely fix it, but maybe others might help you further based on my findings:
First of all, the process within the button click event is a different process space as where the parent PowerShell host runs in. This can be easily proven but revealing the $hwhd with Write-Host $hwnd in the Show-Process function and also calling the Show-Process function prior calling the ShowDialog:
Show-Process -Process (Get-Process -Id $pid)
[void]$Form.ShowDialog()
In other words: to fix this part, you will need to the catch the parent $Pid from the PowerShell window first:
$Button1.Add_Click({
Show-Process -Process $MyProcess
})
$Form.controls.AddRange(#($Button1))
$MyProcess = Get-Process -Id $pid
Show-Process -Process $MyProcess
[void]$Form.ShowDialog()
The above snippet works, but as soon I remove (or comment out) the line Show-Process -Process $MyProcess (at the host level), it breaks again...
As you've discovered, .MainWindowHandle is not a static property (from the linked docs; emphasis added):
The main window is the window opened by the process that currently has the focus [...]
Therefore, what the value of the current process' .MainWindowHandle property changes from the console-window handle to the WinForms window while the form is being displayed.[1]
Caching the console-window handle before you display the form is definitely an option, but there's an easier way, given that you're already using Add-Member with WinAPI P/Invoke declarations: The GetConsoleWindow() WinAPI function always returns the current process' console-window handle.
Additionally, your $Forms form instance has a .Handle property, which directly returns the form's window handle - no (Get-Process -Id $pid).MainWindowHandle call needed.
The following solution therefore needs no global or script-level variables and confines querying the window handles to the button-click event handler:
# P/Invoke signatures - note the addition of GetConsoleWindow():
$sig = '
[DllImport("user32.dll")] public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);
[DllImport("user32.dll")] public static extern int SetForegroundWindow(IntPtr hwnd);
[DllImport("kernel32.dll")] public static extern IntPtr GetConsoleWindow();'
$type = Add-Type -MemberDefinition $sig -Name WindowAPI -PassThru
Add-Type -AssemblyName System.Windows.Forms
$Form = New-Object system.Windows.Forms.Form -Property #{
ClientSize = '446,266'
text = "Form"
}
$Button1 = New-Object system.Windows.Forms.Button -Property #{
text = "Test"
location = New-Object System.Drawing.Point(75, 29)
}
$Button1.Add_Click({
# Get this form's window handle.
$handleForm = $Form.Handle # More generically: $this.FindForm().Handle
# Get the console window's handle.
$handleConsole = $type::GetConsoleWindow()
# Activate the console window and prompt the user.
$null = $type::ShowWindowAsync($handleConsole, 4); $null = $type::SetForegroundWindow($handleConsole)
Read-Host -Prompt "Please Enter a Value"
# Reactivate this form.
$null = $type::ShowWindowAsync($handleForm, 4); $null = $type::SetForegroundWindow($handleForm)
})
$Form.controls.AddRange(#($Button1))
$null = $Form.ShowDialog()
[1] Note that a cached process object doesn't dynamically update its .MainWindowHandle value; you have to call .Refresh() manually.
Because iRon's solution caches the current-process object before displaying the form, it still happens to reflect the console-window handle inside the button-click handler.

Speed up PowerShell script for Windows Registry search (currently 30 minutes)

I'm working on a script for use in Windows 7 and Windows 10 for a Windows Registry search in HKLM:\Software\Classes. So far my code works, but it's extremely slow. It takes about 30 minutes to complete.
I need to use Set-Location also to avoid an error with Get-ItemProperty, which occurs because the $path is not a valid object.
How can I speed this code up? What's wrong?
File regsearch.ps1 (Mathias R. Jessen's answer applied)
Function Get-RegItems
{
Param(
[Parameter(Mandatory=$true)]
[string]$path,
[string]$match)
#Set Local Path and ignore wildcard (literalpath)
Set-Location -literalpath $path
$d = Get-Item -literalpath $path
# If more than one value -> process
If ($d.Valuecount -gt 0) {
$d |
# Get unkown property
Select-Object -ExpandProperty Property |
ForEach {
$val = (Get-ItemProperty -Path . -Name $_).$_
#if Filter $match found, generate ReturnObject
if (($_ -match $match) -or ($val -match $match ) -or ($path-match $match)) {
New-Object psobject -Property #{ “key”=$path; “property”=$_; “value” = $val ;}
}
}
}
} #end function Get-RegItems
Function RegSearch
{
Param(
[Parameter(Mandatory=$true)]
[string]$path,
[string]$match)
# Expand $path if necessary to get a valid object
if ($path.Indexof("HKEY") -ne "-1" -and $path.Indexof("Registry::") -eq "-1" ) {
$path = "Microsoft.PowerShell.Core\Registry::" +$path
}
# Retrieve items of the main key
Get-RegItems -path $path -match $match
# Retrieve items of all child keys
Get-ChildItem $path -Recurse -ErrorAction SilentlyContinue |
ForEach {
Get-RegItems -path $_.PsPath -match $match
}
} #end function RegSearch
#$search = "HKCU:\SOFTWARE\Microsoft\Office"
$searchkey = ‘HKLM:\SOFTWARE\Microsoft\Office\’
#$searchkey = "HKLM:\Software\Classes\"
$pattern = "EventSystem"
cls
$result = #()
Measure-Command {$result = Regsearch -path $searchkey -match $pattern }
# TESTING
#$t = #( "Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\Software\Classes",
# "HKLM:\Software\Classes\Wow6432Node\CLSID\",
# "HKCU:\SOFTWARE\Microsoft\Office\",
# "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Office")
#cls
#$t |ForEach { Get-RegItems -path $_ } | fl
if ($result.Count) {
$result
"Count: {0}" -f ($result.Count-1)
}
else {
"Path: {0} `nNo Items found" -f $searchkey
}
I accepted the challenge and made it "as fast as possible".
Now it is even faster than REGEDIT or any other tool.
The below sample lasts 11 seconds to parse the complete OFFICE-key and all subkeys.
In addition, it also searches for string-matches in REG-BINARY etc.
Enjoy!
# carsten.giese#googlemail.com
# reference: https://msdn.microsoft.com/de-de/vstudio/ms724875(v=vs.80)
cls
remove-variable * -ea 0
$ErrorActionPreference = "stop"
$signature = #'
[DllImport("advapi32.dll")]
public static extern Int32 RegOpenKeyEx(
UInt32 hkey,
StringBuilder lpSubKey,
int ulOptions,
int samDesired,
out IntPtr phkResult
);
[DllImport("advapi32.dll")]
public static extern Int32 RegQueryInfoKey(
IntPtr hKey,
StringBuilder lpClass, Int32 lpCls, Int32 spare,
out int subkeys, out int skLen, int mcLen, out int values,
out int vNLen, out int mvLen, int secDesc,
out System.Runtime.InteropServices.ComTypes.FILETIME lpftLastWriteTime
);
[DllImport("advapi32.dll", CharSet = CharSet.Unicode)]
public static extern Int32 RegEnumValue(
IntPtr hKey,
int dwIndex,
IntPtr lpValueName,
ref IntPtr lpcchValueName,
IntPtr lpReserved,
out IntPtr lpType,
IntPtr lpData,
ref int lpcbData
);
[DllImport("advapi32.dll", CharSet = CharSet.Unicode)]
public static extern Int32 RegEnumKeyEx(
IntPtr hKey,
int dwIndex,
IntPtr lpName,
ref int lpcName,
IntPtr lpReserved,
IntPtr lpClass,
int lpcClass,
out System.Runtime.InteropServices.ComTypes.FILETIME lpftLastWriteTime
);
[DllImport("advapi32.dll")]
public static extern Int32 RegCloseKey(IntPtr hkey);
'#
$reg = add-type $signature -Name reg -Using System.Text -PassThru
$marshal = [System.Runtime.InteropServices.Marshal]
function search-RegistryTree($path) {
# open the key:
[IntPtr]$hkey = 0
$result = $reg::RegOpenKeyEx($global:hive, $path, 0, 25,[ref]$hkey)
if ($result -eq 0) {
# get details of the key:
$subKeyCount = 0
$maxSubKeyLen = 0
$valueCount = 0
$maxNameLen = 0
$maxValueLen = 0
$time = $global:time
$result = $reg::RegQueryInfoKey($hkey,$null,0,0,[ref]$subKeyCount,[ref]$maxSubKeyLen,0,[ref]$valueCount,[ref]$maxNameLen,[ref]$maxValueLen,0,[ref]$time)
if ($result -eq 0) {
$maxSubkeyLen += $maxSubkeyLen+1
$maxNameLen += $maxNameLen +1
$maxValueLen += $maxValueLen +1
}
# enumerate the values:
if ($valueCount -gt 0) {
$type = [IntPtr]0
$pName = $marshal::AllocHGlobal($maxNameLen)
$pValue = $marshal::AllocHGlobal($maxValueLen)
foreach ($index in 0..($valueCount-1)) {
$nameLen = $maxNameLen
$valueLen = $maxValueLen
$result = $reg::RegEnumValue($hkey, $index, $pName, [ref]$nameLen, 0, [ref]$type, $pValue, [ref]$valueLen)
if ($result -eq 0) {
$name = $marshal::PtrToStringUni($pName)
$value = switch ($type) {
1 {$marshal::PtrToStringUni($pValue)}
2 {$marshal::PtrToStringUni($pValue)}
3 {$b = [byte[]]::new($valueLen)
$marshal::Copy($pValue,$b,0,$valueLen)
if ($b[1] -eq 0 -and $b[-1] -eq 0 -and $b[0] -ne 0) {
[System.Text.Encoding]::Unicode.GetString($b)
} else {
[System.Text.Encoding]::UTF8.GetString($b)}
}
4 {$marshal::ReadInt32($pValue)}
7 {$b = [byte[]]::new($valueLen)
$marshal::Copy($pValue,$b,0,$valueLen)
$msz = [System.Text.Encoding]::Unicode.GetString($b)
$msz.TrimEnd(0).split(0)}
11 {$marshal::ReadInt64($pValue)}
}
if ($name -match $global:search) {
write-host "$path\$name : $value"
$global:hits++
} elseif ($value -match $global:search) {
write-host "$path\$name : $value"
$global:hits++
}
}
}
$marshal::FreeHGlobal($pName)
$marshal::FreeHGlobal($pValue)
}
# enumerate the subkeys:
if ($subkeyCount -gt 0) {
$subKeyList = #()
$pName = $marshal::AllocHGlobal($maxSubkeyLen)
$subkeyList = foreach ($index in 0..($subkeyCount-1)) {
$nameLen = $maxSubkeyLen
$result = $reg::RegEnumKeyEx($hkey, $index, $pName, [ref]$nameLen,0,0,0, [ref]$time)
if ($result -eq 0) {
$marshal::PtrToStringUni($pName)
}
}
$marshal::FreeHGlobal($pName)
}
# close:
$result = $reg::RegCloseKey($hkey)
# get Tree-Size from each subkey:
$subKeyValueCount = 0
if ($subkeyCount -gt 0) {
foreach ($subkey in $subkeyList) {
$subKeyValueCount += search-RegistryTree "$path\$subkey"
}
}
return ($valueCount+$subKeyValueCount)
}
}
$timer = [System.Diagnostics.Stopwatch]::new()
$timer.Start()
# setting global variables:
$search = "enterprise"
$hive = [uint32]"0x80000002" #HKLM
$subkey = "SOFTWARE\Microsoft\Office"
$time = New-Object System.Runtime.InteropServices.ComTypes.FILETIME
$hits = 0
write-host "We start searching for pattern '$search' in Registry-Path '$subkey' ...`n"
$count = search-RegistryTree $subkey
$timer.stop()
$sec = [int](100 * $timer.Elapsed.TotalSeconds)/100
write-host "`nWe checked $count reg-values in $sec seconds. Number of hits = $hits."
The single biggest improvement you can make here is changing:
Set-Location -literalpath $path
$d= Get-Item .
to
$d = Get-Item -LiteralPath $path
Manipulating the location stack for each key in the hierarchy introduces A LOT of unnecessary overhead
User function call overhead (scriptblocks included) is extremely big (e.g. 0.1-1ms). This becomes a very serious issue when the function is executed thousands/millions of times. Surprisingly, it's not mentioned in optimization-related articles (at least I've never seen it and I googled this topic a lot).
Unfortunately, the only only real solution to this particular issue is to inline the code at the cost of duplication and reduced readability.
Optimization should include code profiling.
PowerShell doesn't have a code profiler so you'll need to do it manually with Measure-Command.
Use System.Diagnostics.Stopwatch inside loops to display the accumulated time:
# global stopwatch
$sw1 = [Diagnostics.Stopwatch]::new()
$sw2 = [Diagnostics.Stopwatch]::new()
............
forEach(....) {
........
$sw1.start()
........
$sw1.stop()
........
$sw2.start()
........
$sw2.stop()
........
}
............
echo $sw1.ElapsedMilliseconds, $sw2.ElapsedMilliseconds
Here is a faster version of you sample-script.
Lasts ca. 1 minute on my machine.
If you need it faster, then you need to work with advapi32.dll-Pinvokes, but then
it will end quite complex.
Function Get-RegItems {
Param(
[Parameter(Mandatory=$true)]
[string]$path,
[string]$match
)
#write-host $path.Substring(30)
$key = Get-Item -literalpath $path
ForEach ($entry in $key.Property) {
$value = $key.GetValue($entry)
if (($entry -match $match) -or ($value -match $match ) -or ($path -match $match)) {
write-host "key=$path property=$entry value=$value"
}
}
}
Function RegSearch {
Param(
[Parameter(Mandatory=$true)]
[string]$path,
[string]$match
)
Get-RegItems -path $path -match $match
ForEach ($item in get-ChildItem -literalpath $path -ea 0) {
RegSearch -path $item.PsPath -match $match
}
}
cls
Remove-Variable * -ea 0
[System.GC]::Collect()
$searchkey =‘HKLM:\SOFTWARE\Microsoft\Office’
$pattern = "EventSystem"
measure-command {
$result = RegSearch -path $searchkey -match $pattern
}
Don't use the registry drive provider, if you want it faster.
I've also read classes with static methods are faster.

File System Watcher stops working when converting Word doc/docx files to PDF

I have a Powershell script for automatic converting .doc/.docx files to *.pdf.
The script is running well for the first file. But if I put another file in the watched folder, the watcher doesn't trigger an event.
Here is the complete script. If I comment out the all $doc variables, the script is running multiple times without any problems. Did I ignore/overlook something?
$watcher = New-Object System.IO.FileSystemWatcher
$watcher.Path = "$Env:DropboxRoot"
$watcher.Filter = "*.doc*"
$watcher.IncludeSubdirectories = $true
$watcher.EnableRaisingEvents = $true
Add-type -AssemblyName Microsoft.Office.Interop.Word
$action = {
$name = (get-item $Event.SourceEventArgs.FullPath).BaseName
### DON'T PROCESS WORD BACKUP FILES (START WITH A TILDE ~)
if(!($name.startsWith("~"))){
write-host Triggered event from $Event.SourceEventArgs.FullPath
$inputFilePath = $Event.SourceEventArgs.FullPath
$parentPath = (get-item $inputFilePath).Directory
$filename = (get-item $inputFilePath).BaseName
$pdfDir = "$parentPath\PDF"
if(!(Test-Path -Path $pdfDir)){
New-Item -ItemType directory -Path $pdfDir
}
###Execute PDF generate script
write-host Create word object
$word = New-Object -ComObject "Word.Application"
######define the parameters######
write-host Define parameters
$wdExportFormat =[Microsoft.Office.Interop.Word.WdExportFormat]::wdExportFormatPDF
$OpenAfterExport = $false
$wdExportOptimizeFor = [Microsoft.Office.Interop.Word.WdExportOptimizeFor]::wdExportOptimizeForOnScreen
$wdExportItem = [Microsoft.Office.Interop.Word.WdExportItem]::wdExportDocumentContent
$IncludeDocProps = $true
$KeepIRM = $false #Don't export Inormation Rights Management informations
$wdExportCreateBookmarks = [Microsoft.Office.Interop.Word.WdExportCreateBookmarks]::wdExportCreateWordBookmarks #Keep bookmarks
$DocStructureTags = $true #Add additional data for screenreaders
$BitmapMissingFonts = $true
$UseISO19005_1 = $true #Export as PDF/A
$outputFilePath = $pdfDir + "\" + $filename + ".pdf"
$doc = $word.Documents.Open($inputFilePath)
$doc.ExportAsFixedFormat($OutputFilePath,$wdExportFormat,$OpenAfterExport,`
$wdExportOptimizeFor,$wdExportRange,$wdStartPage,$wdEndPage,$wdExportItem,$IncludeDocProps,`
$KeepIRM,$wdExportCreateBookmarks,$DocStructureTags,$BitmapMissingFonts,$UseISO19005_1)
$doc.Close()
$word.Quit()
[void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($doc)
[void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($word)
[GC]::Collect()
[GC]::WaitForPendingFinalizers()
}
}
$created = Register-ObjectEvent $watcher -EventName "Created" -Action $action
$renamed = Register-ObjectEvent $watcher -EventName "Renamed" -Action $action
while($true) {
sleep 5
}`
Your script has a few issues, that more debugging logic could find.
In some cases, (Get-Item System.Management.Automation.PSEventArgs.SourceEventArgs.FullPath) returns null. For unknown reasons, this seems to happen once for every document that gets converted. Perhaps it has to do with the "~Temp" files.
Subsequently, if(!($name.startsWith("~") will throw an exception.
When you use $inputFilePath = $Event.SourceEventArgs.FullPath, your variable is a FileInfo, and really you want to pass a string to $word.Documents.Open($inputFilePath).
Lastly, sometimes BaseName is null. Not sure why but the code could test for that or use other means to dissect the FullPath to get names and path parts.
All that said, once you get this working, my personal experience is that calling the COM object on Word to do this conversion in PowerShell is unreliable (Word hangs, ~Temp files get left behind, you have to kill Word from task manager, the COM calls in PowerShell never return). My testing shows that calling a C# console app to do the conversion is much more reliable. You could write this directory watcher and converter completely in C# and accomplish the same task.
Assuming you still want to combine the two, a PowerShell watcher, and a C# Word to PDF converter, below is a solution I came up with. The script runs for about a minute so you can test in the ISE or Console. From the Console press a key to exit. Before exiting, the script exits cleanly by unregistering the events which is quite helpful while testing in the ISE. Change this accordingly for how you intend to run the script.
PowerShell watcher
$watcher = New-Object System.IO.FileSystemWatcher
$watcher.Path = "d:\test\docconvert\src"
$watcher.Filter = "*.doc*"
$watcher.IncludeSubdirectories = $true
$watcher.EnableRaisingEvents = $true
# copy this somehwere appropriate
# perhaps in same directory as your script
# put on a read-only share, etc.
$wordToPdf = 'd:\test\docconvert\WordToPdf\WordToPdf\bin\Debug\WordToPdf.exe'
$action = {
try
{
Write-Host "Enter action # $(Get-Date)"
$fullPathObject = (Get-Item $Event.SourceEventArgs.FullPath)
if (!($fullPathObject))
{
Write-Host "(Get-Item $Event.SourceEventArgs.FullPath) returned null."
return
}
$fullPath = ($fullPathObject).ToString()
Write-Host "Triggered event from $fullPath"
$fileName = Split-Path $FullPath -Leaf
if ($fileName -and ($fileName.StartsWith("~")))
{
Write-Host "Skipping temp file"
return
}
# put pdf in same dir as the file
# can be changed, but a lot easier to test this way
$pdfDir = Split-Path $FullPath -Parent
$baseName = [System.IO.Path]::GetFileNameWithoutExtension($fileName)
$outputFilePath = Join-Path $pdfDir $($baseName + ".pdf")
Write-Host "outputFilePath is: '$outputFilePath'"
# call c# WordToPdf to do conversion because
# it is way more reliable than similar calls
# from PowerShell
& $wordToPdf $fullPath $outputFilePath
if ($LASTEXITCODE -ne 0)
{
Write-Host "Conversion result: FAIL"
}
else
{
Write-Host "Conversion result: OK"
}
}
catch
{
Write-Host "Exception from ACTION:`n$($_ | Select *)"
}
finally
{
Write-Host "Exit action # $(Get-Date)"
}
}
$created = Register-ObjectEvent $watcher -EventName "Created" -Action $action
$renamed = Register-ObjectEvent $watcher -EventName "Renamed" -Action $action
$count = 12
while($count--) {
Write-Output "run/sleep ($count)..."
sleep 5
# will exit from console, not ISE
if ([console]::KeyAvailable) {
$key = [console]::ReadKey()
break
}
}
$created | % {Unregister-Event $_.Name}
$renamed | % {Unregister-Event $_.Name}
C# WordToPdf converter
add appropriate error checking for the arguments...
add Reference to COM Microsoft.Office.Interop.Word
using System;
using Microsoft.Office.Interop.Word;
namespace WordToPdf
{
class Program
{
static int Main(string[] args)
{
Console.WriteLine($"Converting: {args[0]} to {args[1]}");
var conversion = new DocumentConversion();
bool result = conversion.WordToPdf(args[0], args[1]);
if (result)
{
return 0;
}
else {
return 1;
}
}
}
public class DocumentConversion
{
private Microsoft.Office.Interop.Word.Application Word;
private object Unknown = Type.Missing;
private object True = true;
private object False = false;
public bool WordToPdf(object Source, object Target)
{
bool ret = true;
if (Word == null) Word = new Microsoft.Office.Interop.Word.Application();
try
{
Word.Visible = false;
Word.Documents.Open(ref Source, ref Unknown,
ref True, ref Unknown, ref Unknown,
ref Unknown, ref Unknown, ref Unknown,
ref Unknown, ref Unknown, ref Unknown,
ref Unknown, ref Unknown, ref Unknown,
ref Unknown, ref Unknown);
Word.Application.Visible = false;
Word.WindowState = WdWindowState.wdWindowStateMinimize;
#if false
object saveFormat = Microsoft.Office.Interop.Word.WdSaveFormat.wdFormatPDF;
Word.ActiveDocument.SaveAs(ref Target, ref saveFormat,
ref Unknown, ref Unknown, ref Unknown,
ref Unknown, ref Unknown, ref Unknown,
ref Unknown, ref Unknown, ref Unknown,
ref Unknown, ref Unknown, ref Unknown,
ref Unknown, ref Unknown);
#else
Word.ActiveDocument.ExportAsFixedFormat(
(string)Target, WdExportFormat.wdExportFormatPDF,
false, WdExportOptimizeFor.wdExportOptimizeForOnScreen,
WdExportRange.wdExportAllDocument, 0, 0,
WdExportItem.wdExportDocumentContent, true, false,
WdExportCreateBookmarks.wdExportCreateWordBookmarks,
true, true, true);
#endif
}
catch (Exception e)
{
Console.WriteLine(e.Message);
ret = false;
}
finally
{
if (Word != null)
{
// close the application
Word.Quit(ref Unknown, ref Unknown, ref Unknown);
}
}
return ret;
}
}
}

Powershell Debug Output

I have a powershell script that creates some DotNetZip ZIP files on a persistent connection on multiple servers, then uses Start-BitsTransfer to move the ZIP files from the remote servers to the local.
I run pretty much the same script on two different servers, on one it barely prints anything to the screen. On the other, it outputs a LOT - binary looking stuff. Is this because there is some kind of debug setting that is turned on on the server that is outputting all of this info? Is there a way I can turn it off? I'd rather it be more clean like the first server that runs the script.
Thank you!
-Jim
Here is almost the entire script (without my servernames in the $webServers array:
Import-Module BitsTransfer
foreach($i in $webServers) {
if (!(Test-Path -path \\$i\d$\newDeploy)) {
New-Item \\$i\d$\newDeploy -type directory
}
if (!(Test-Path -path \\$i\d$\newDeploy\backup)) {
New-Item \\$i\d$\newDeploy\backup -type directory
}
if(!(Test-Path \\$i\d$\newDeploy\Ionic.Zip.dll)) {
Start-BitsTransfer -Source \\$webDeployServer\d$\newDeploy\Ionic.Zip.dll -Destination \\$i\d$\newDeploy
}
}
foreach($i in $webServers) {
$sessionForI = New-PSSession -computername $i
Invoke-Command -Session $sessionForI -ScriptBlock {
if ((Test-Path D:\\newDeploy\\backup\\OffM.zip)) {
Remove-Item D:\\newDeploy\\backup\\OffM.zip
}
[System.Reflection.Assembly]::LoadFrom("D:\\newDeploy\\Ionic.Zip.dll");
$zipfile = new-object Ionic.Zip.ZipFile
$e = $zipfile.AddSelectedFiles("name != '*.e2e'","D:\inetpub\wwwroot\OffM", "OffM",1)
$e = $zipfile.AddSelectedFiles("name != '*.e2e'","D:\inetpub\wwwroot\PaEnterprise", "PaEnterprise",1)
$zipfile.Save("D:\\newDeploy\\backup\\OffM.zip")
$zipfile.Dispose()
if ((Test-Path D:\\newDeploy\\backup\\Others.zip)) {
Remove-Item D:\\newDeploy\\backup\\Others.zip
}
[System.Reflection.Assembly]::LoadFrom("D:\\newDeploy\\Ionic.Zip.dll");
$zipfile = new-object Ionic.Zip.ZipFile
$e = $zipfile.AddSelectedFiles("name != '*.e2e'","D:\inetpub\wwwroot\MstrInt-PO", "MstrInt-PO",1)
$e = $zipfile.AddSelectedFiles("name != '*.e2e'","D:\inetpub\wwwroot\Maint", "Maint",1)
$zipfile.Save("D:\\newDeploy\\backup\\Others.zip")
$zipfile.Dispose()
if ((Test-Path D:\\newDeploy\\backup\\PPO.zip)) {
Remove-Item D:\\newDeploy\\backup\\PPO.zip
}
[System.Reflection.Assembly]::LoadFrom("D:\\newDeploy\\Ionic.Zip.dll");
$zipfile = new-object Ionic.Zip.ZipFile
$e = $zipfile.AddSelectedFiles("name != '*.e2e'","D:\inetpub\wwwroot\HC", "HC",1)
$e = $zipfile.AddSelectedFiles("name != '*.e2e'","D:\inetpub\wwwroot\PaOn", "PaOn",1)
if($i -eq 'PYRALNWSP02V') {
$e = $zipfile.AddSelectedFiles("name != '*.e2e'","D:\inetpub\wwwroot\HearPl", "HearPl",1)
} else {
$e = $zipfile.AddSelectedFiles("name != '*.e2e'","D:\inetpub\wwwroot\HearPaPlu", "HearPaPlu",1)
}
$zipfile.Save("D:\\newDeploy\\backup\\PPO.zip")
$zipfile.Dispose()
if ((Test-Path D:\\newDeploy\\backup\\TiMan.zip)) {
Remove-Item D:\\newDeploy\\backup\\TiMan.zip
}
[System.Reflection.Assembly]::LoadFrom("D:\\newDeploy\\Ionic.Zip.dll");
$zipfile = new-object Ionic.Zip.ZipFile
$e = $zipfile.AddSelectedFiles("name != '*.e2e'","D:\inetpub\wwwroot\TiManView", "TiManView",1)
$e = $zipfile.AddSelectedFiles("name != '*.e2e'","D:\inetpub\wwwroot\TiManOnline", "TiManOnline",1)
$e = $zipfile.AddSelectedFiles("name != '*.e2e'","D:\inetpub\wwwroot\TiManPOne", "TiManPOne",1)
$zipfile.Save("D:\\newDeploy\\backup\\TiMan.zip")
$zipfile.Dispose()
}
remove-PSSession -session $sessionForI
}
foreach($i in $webServers) {
if(!(Test-Path -path D:\newDeploy\backup\$i)) {
New-Item D:\newDeploy\backup\$i -type directory
}
Start-BitsTransfer -Source \\$i\d$\newDeploy\backup\OffM.zip -Destination D:\newDeploy\backup\$i
Start-BitsTransfer -Source \\$i\d$\newDeploy\backup\Others.zip -Destination D:\newDeploy\backup\$i
Start-BitsTransfer -Source \\$i\d$\newDeploy\backup\PPO.zip -Destination D:\newDeploy\backup\$i
Start-BitsTransfer -Source \\$i\d$\newDeploy\backup\TiMan.zip -Destination D:\newDeploy\backup\$i
}
foreach($i in $webServers) {
Remove-Item \\$i\d$\newDeploy\backup\OffM.zip
Remove-Item \\$i\d$\newDeploy\backup\Others.zip
Remove-Item \\$i\d$\newDeploy\backup\PPO.zip
Remove-Item \\$i\d$\newDeploy\backup\TiMan.zip
}
[System.Reflection.Assembly]::LoadFrom("D:\\newDeploy\\Ionic.Zip.dll");
$directoryToZip = "D:\newDeploy\backup"
$date = get-date -format "M-d-yyyy"
$zipfile = new-object Ionic.Zip.ZipFile
$e = $zipfile.AddSelectedFiles("name != '*.e2e'",$directoryToZip, "",1)
$zipfile.Save("D:\\newDeploy\\backup\\"+$date+"_WEBbackup.zip")
$zipfile.Dispose()
There is a debug messages setting like that - $DebugPreference
It's default is SilentlyContinue. See if it is set to something else.
It would help if you also showed the difference in output. It could also be a verbose output as controlled by the $VerbosePreference
Look here to know about the preference varibales - http://technet.microsoft.com/en-us/library/dd347731.aspx
Update:
Add a [void] before [System.Reflection.Assembly]::LoadFrom statements so that the output doesn't pollute the script output.

Resources