├── .editorconfig ├── .markdownlint.json ├── .vscode ├── extensions.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── Native ├── ConsoleAPI.cs └── ConsoleAPI.ps1 ├── PSScriptAnalyzerSettings.psd1 ├── PSWinGlue.psd1 ├── PSWinGlue.psm1 ├── README.md └── Scripts ├── Add-VpnCspConnection.ps1 ├── Find-OrphanDependencyPackages.ps1 ├── Get-ControlledGpoStatus.ps1 ├── Get-Fonts.ps1 ├── Get-InstalledPrograms.ps1 ├── Get-TaskSchedulerEvent.ps1 ├── Hide-SilverlightUpdates.ps1 ├── Install-ExcelAddin.ps1 ├── Install-Font.ps1 ├── Install-VSTOAddin.ps1 ├── Register-MicrosoftUpdate.ps1 ├── Remove-AlternateDataStream.ps1 ├── Remove-OrphanDependencyPackages.ps1 ├── Set-SharedPCMode.ps1 ├── Sort-RegistryExport.ps1 ├── Uninstall-Font.ps1 ├── Uninstall-ObsoleteModule.ps1 ├── Uninstall-VSTOAddin.ps1 ├── Update-GitRepository.ps1 └── Update-OneDriveSetup.ps1 /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig 2 | # http://EditorConfig.org 3 | 4 | # Don't search any further up the directory tree 5 | root = true 6 | 7 | # Baseline 8 | [*] 9 | charset = utf-8 10 | indent_style = space 11 | indent_size = 4 12 | trim_trailing_whitespace = true 13 | insert_final_newline = true 14 | 15 | # Markdown 16 | [*.md] 17 | trim_trailing_whitespace = false 18 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": true, 3 | "heading-style": { "style": "setext_with_atx" }, 4 | "no-trailing-spaces": { "br_spaces": 2 }, 5 | "line-length": false, 6 | "no-trailing-punctuation": { "punctuation": ".,;:!" }, 7 | "no-inline-html": { "allowed_elements": ["br", "table", "td", "tr"] } 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "davidanson.vscode-markdownlint", 4 | "editorconfig.editorconfig", 5 | "ms-vscode.powershell", 6 | "yzhang.markdown-all-in-one" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // Important to disable for EditorConfig to apply correctly 3 | // See: https://github.com/editorconfig/editorconfig-vscode/issues/153 4 | "files.trimTrailingWhitespace": false, 5 | 6 | // Markdown table of contents generation 7 | "markdown.extension.toc.levels": "2..2", 8 | "markdown.extension.toc.slugifyMode": "github", 9 | 10 | // PowerShell script analysis 11 | "powershell.scriptAnalysis.settingsPath": "PSScriptAnalyzerSettings.psd1", 12 | 13 | // PowerShell code formatting 14 | "powershell.codeFormatting.autoCorrectAliases": true, 15 | "powershell.codeFormatting.newLineAfterCloseBrace": false, 16 | "powershell.codeFormatting.pipelineIndentationStyle": "IncreaseIndentationForFirstPipeline", 17 | "powershell.codeFormatting.trimWhitespaceAroundPipe": true, 18 | "powershell.codeFormatting.useCorrectCasing": true, 19 | "powershell.codeFormatting.whitespaceBetweenParameters": true, 20 | 21 | "[powershell]": { 22 | "editor.formatOnSave": true, 23 | "editor.rulers": [79] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | v0.6.11 5 | ------- 6 | 7 | - `Get-Fonts`: Skip checking for missing fonts when none found in scope 8 | - `Install-Font`: Ensure global `$PSWinGluePackageCounter` is instantiated 9 | - `Install-Font`: Remove unnecessary `$PartCounter` and `$PartUriRaw` variables 10 | - `Install-Font`: Return an empty list from font enumeration if none found 11 | - `Install-Font`: Skip font hash checks if selected scope has no fonts 12 | 13 | v0.6.10 14 | ------- 15 | 16 | - Update `docs.microsoft.com` links to `learn.microsoft.com` 17 | - `Find-OrphanDependencyPackages`: Fix handling of empty result sets 18 | 19 | v0.6.9 20 | ------ 21 | 22 | - Show a warning on skipping import of existing functions 23 | - `Find-OrphanDependencyPackages`: Fix Dotnet_CLI_SharedHost regex 24 | 25 | v0.6.8 26 | ------ 27 | 28 | - `Install-Font`: Internal changes to avoid handles to font files being kept open 29 | 30 | v0.6.7 31 | ------ 32 | 33 | - Add `Sort-RegistryExport` function 34 | - `Set-SharedPCMode`: Add support for additional settings 35 | 36 | v0.6.6 37 | ------ 38 | 39 | - `Get-Fonts`: Gracefully handle missing *Fonts* folder or registry key (GH #8) 40 | - `Install-Font`: Gracefully handle missing *Fonts* folder or registry key (GH #8) 41 | 42 | v0.6.5 43 | ------ 44 | 45 | - `Install-Font`: Add `-UninstallExisting` parameter (GH #6) 46 | - `Uninstall-Font`: Add `-IgnoreNotPresent` parameter (GH #7) 47 | 48 | v0.6.4 49 | ------ 50 | 51 | - `Find-OrphanDependencyPackages`: Add awareness of *Visual Studio* package cache 52 | - `Install-Font`: Remove warning on finding unregistered fonts 53 | 54 | v0.6.3 55 | ------ 56 | 57 | - `Uninstall-ObsoleteModule`: Retain latest `PowerShellGet` v2 & v3 versions 58 | - `Uninstall-ObsoleteModule`: Fix `-Name` bug when module has a single version 59 | 60 | v0.6.2 61 | ------ 62 | 63 | - `Uninstall-ObsoleteModule`: Fix incorrect `ProgressPreference` value 64 | 65 | v0.6.1 66 | ------ 67 | 68 | - `Uninstall-ObsoleteModule`: Improve `PowerShellGet` version detection 69 | 70 | v0.6.0 71 | ------ 72 | 73 | New functions: 74 | 75 | - `Find-OrphanDependencyPackages` 76 | - `Install-VSTOAddin` (previously `Manage-VSTOAddin`) 77 | - `Remove-OrphanDependencyPackages` 78 | - `Uninstall-VSTOAddin` (previously `Manage-VSTOAddin`) 79 | 80 | Removed functions: 81 | 82 | - `Manage-VSTOAddin` (split into `Install` and `Uninstall` functions) 83 | 84 | Added built-in help: 85 | 86 | - `Add-VpnCspConnection` 87 | - `Get-InstalledPrograms` 88 | - `Hide-SilverlightUpdates` 89 | - `Install-ExcelAddin` 90 | - `Register-MicrosoftUpdate` 91 | - `Update-GitRepository` 92 | 93 | Major code clean-up: 94 | 95 | - `Add-VpnCspConnection` 96 | - `Get-Fonts` 97 | - `Hide-SilverlightUpdates` 98 | - `Install-ExcelAddin` 99 | - `Install-Font` 100 | - `Register-MicrosoftUpdate` 101 | - `Remove-AlternateDateStream` 102 | - `Uninstall-Font` 103 | - `Uninstall-ObsoleteModule` 104 | - `Update-GitRepository` 105 | 106 | Minor code clean-up: 107 | 108 | - `Get-InstalledPrograms` 109 | - `Get-TaskSchedulerEvent` 110 | - `Set-SharedPCMode` 111 | - `Update-OneDriveSetup` 112 | 113 | Additional changes: 114 | 115 | - `ConsoleAPI`: Add all missing function signatures 116 | - `ConsoleAPI`: Add namespace (`PSWinGlue`) and class (`ConsoleAPI`) 117 | - `Uninstall-ObsoleteModule`: Add PowerShellGet v3 support (currently in beta) 118 | - Add check we're running on Windows for applicable functions 119 | - Replace `-RunAsAdministrator` with equivalent check in functions 120 | - Minor code clean-up & developer tooling improvements 121 | 122 | v0.5.7 123 | ------ 124 | 125 | - `Uninstall-ObsoleteModule`: Replace `ValidateRangeKind` attribute for PowerShell < 6.2 compatibility 126 | 127 | v0.5.6 128 | ------ 129 | 130 | - `Uninstall-ObsoleteModule`: Add progress bar support 131 | 132 | v0.5.5 133 | ------ 134 | 135 | - `Get-InstalledPrograms`: Fix incorrect access to `$PSVersionTable` causing errors under PowerShell Core 136 | 137 | v0.5.4 138 | ------ 139 | 140 | - `Get-InstalledPrograms`: Fall back to last write time of uninstall registry key 141 | 142 | v0.5.3 143 | ------ 144 | 145 | - `Hide-SilverlightUpdates`: Sets all Silverlight updates from Windows Update to Hidden 146 | 147 | v0.5.2 148 | ------ 149 | 150 | - `Uninstall-ObsoleteModule`: Add workaround for `Get-Module` ignoring modules with names matching locales 151 | - `Uninstall-ObsoleteModule`: Don't throw an exception on `Uninstall-Module` returning `ModuleIsInUse` 152 | - Apply code formatting 153 | 154 | v0.5.1 155 | ------ 156 | 157 | - `Update-OneDriveSetup`: Quote default user profile hive path for reg.exe 158 | - Syntax fixes for older PowerShell versions 159 | - Performance optimisations around array use 160 | 161 | v0.5.0 162 | ------ 163 | 164 | - Added `Add-VpnCspConnection` to add a VPN connection via the VPNv2 CSP 165 | - Added `ConsoleAPI.ps1` to add type with P/Invoke signatures for many native Console API functions 166 | - Added `Get-Fonts` to retrieve system or per-user fonts 167 | - Added `Install-ExcelAddin` to install Excel add-ins 168 | - Added `Manage-VSTOAddin` to install or uninstall VSTO add-ins 169 | - Added `Set-SharedPCMode` to configure Windows 10 Shared PC Mode 170 | - Added `Uninstall-Font` to uninstall system or per-user fonts 171 | - Added `Update-OneDriveSetup` to update the in-box OneDrive setup 172 | - `Get-ControlledGpoStatus`: Added built-in command help 173 | - `Get-ControlledGpoStatus`: Support for custom domains & AGPM servers 174 | - `Get-TaskSchedulerEvent`: Fix incorrect built-in help 175 | - Remove unneeded files from published package 176 | - Minor documentation updates & miscellaneous fixes 177 | 178 | v0.4.6 179 | ------ 180 | 181 | - `Get-ControlledGpoStatus`: Matching of GPOs is now done by GUID instead of name 182 | - `Get-ControlledGpoStatus`: Adds check that GPO name and any WMI filter is in sync 183 | - `Get-ControlledGpoStatus`: Computer & user policy version mismatches are now reported separately 184 | 185 | v0.4.5 186 | ------ 187 | 188 | - `Get-InstalledPrograms`: Check if user `Uninstall` key exists 189 | 190 | v0.4.4 191 | ------ 192 | 193 | - Added `#Requires -Version` statement to all scripts 194 | - `Uninstall-ObsoleteModule`: Check `PowerShellGet` module is present 195 | 196 | v0.4.3 197 | ------ 198 | 199 | - `Install-Font`: Overhauled the script and now supports per-user fonts in Windows 10 1809 (or newer) 200 | 201 | v0.4.2 202 | ------ 203 | 204 | - `Uninstall-ObsoleteModule`: Fixed incorrect object type under strict mode in certain scenarios 205 | 206 | v0.4.1 207 | ------ 208 | 209 | - Added `Uninstall-ObsoleteModule` to uninstall obsolete modules installed by `PowerShellGet` 210 | 211 | v0.4 212 | ---- 213 | 214 | - Initial stable release 215 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Samuel Leslie 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Native/ConsoleAPI.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Implements P/Invoke signatures for the Windows Console API 3 | https://learn.microsoft.com/en-us/windows/console/console-functions 4 | */ 5 | 6 | using System; 7 | using System.Runtime.InteropServices; 8 | using System.Text; 9 | 10 | namespace PSWinGlue { 11 | public static class ConsoleAPI { 12 | #region Constants 13 | 14 | // AttachConsole 15 | public const uint ATTACH_PARENT_PROCESS = 4294967295; // -1 16 | 17 | // CONSOLE_FONT_INFOEX 18 | private const int LF_FACESIZE = 32; 19 | 20 | #endregion 21 | 22 | #region Delegates 23 | 24 | public delegate bool HandlerRoutine(ControlEvent dwCtrlType); 25 | 26 | #endregion 27 | 28 | #region Enumerations 29 | 30 | [Flags] 31 | public enum AccessRightsFlags : uint { 32 | GENERIC_WRITE = 0x40000000, 33 | GENERIC_READ = 0x80000000 34 | } 35 | 36 | [Flags] 37 | public enum CharacterAttributes : ushort { 38 | FOREGROUND_BLUE = 0x1, 39 | FOREGROUND_GREEN = 0x2, 40 | FOREGROUND_RED = 0x4, 41 | FOREGROUND_INTENSITY = 0x8, 42 | BACKGROUND_BLUE = 0x10, 43 | BACKGROUND_GREEN = 0x20, 44 | BACKGROUND_RED = 0x40, 45 | BACKGROUND_INTENSITY = 0x80, 46 | COMMON_LVB_LEADING_BYTE = 0x100, 47 | COMMON_LVB_TRAILING_BYTE = 0x200, 48 | COMMON_LVB_GRID_HORIZONTAL = 0x400, 49 | COMMON_LVB_GRID_LVERTICAL = 0x800, 50 | COMMON_LVB_GRID_RVERTICAL = 0x1000, 51 | COMMON_LVB_REVERSE_VIDEO = 0x4000, 52 | COMMON_LVB_UNDERSCORE = 0x8000 53 | } 54 | 55 | public enum ControlEvent : uint { 56 | CTRL_C_EVENT = 0, 57 | CTRL_BREAK_EVENT = 1, 58 | CTRL_CLOSE_EVENT = 2, 59 | CTRL_LOGOFF_EVENT = 5, 60 | CTRL_SHUTDOWN_EVENT = 6 61 | } 62 | 63 | [Flags] 64 | public enum ControlKeyStates : uint { 65 | RIGHT_ALT_PRESSED = 0x1, 66 | LEFT_ALT_PRESSED = 0x2, 67 | RIGHT_CTRL_PRESSED = 0x4, 68 | LEFT_CTRL_PRESSED = 0x8, 69 | SHIFT_PRESSED = 0x10, 70 | NUMLOCK_ON = 0x20, 71 | SCROLLLOCK_ON = 0x40, 72 | CAPSLOCK_ON = 0x80, 73 | ENHANCED_KEY = 0x100 74 | } 75 | 76 | [Flags] 77 | public enum DisplayModeGetFlags : uint { 78 | CONSOLE_FULLSCREEN = 0x1, 79 | CONSOLE_FULLSCREEN_HARDWARE = 0x2 80 | } 81 | 82 | [Flags] 83 | public enum DisplayModeSetFlags : uint { 84 | CONSOLE_FULLSCREEN_MODE = 0x1, 85 | CONSOLE_WINDOWED_MODE = 0x2 86 | } 87 | 88 | [Flags] 89 | public enum EventType : ushort { 90 | KEY_EVENT = 0x1, 91 | MOUSE_EVENT = 0x2, 92 | WINDOW_BUFFER_SIZE_EVENT = 0x4, 93 | MENU_EVENT = 0x8, 94 | FOCUS_EVENT = 0x10 95 | } 96 | 97 | [Flags] 98 | public enum HistoryInfoFlags : uint { 99 | HISTORY_NO_DUP_FLAG = 0x1 100 | } 101 | 102 | [Flags] 103 | public enum InputModeFlags : uint { 104 | ENABLE_PROCESSED_INPUT = 0x1, 105 | ENABLE_LINE_INPUT = 0x2, 106 | ENABLE_ECHO_INPUT = 0x4, 107 | ENABLE_WINDOW_INPUT = 0x8, 108 | ENABLE_MOUSE_INPUT = 0x10, 109 | ENABLE_INSERT_MODE = 0x20, 110 | ENABLE_QUICK_EDIT_MODE = 0x40, 111 | ENABLE_EXTENDED_FLAGS = 0x80, 112 | ENABLE_AUTO_POSITION = 0x100, 113 | ENABLE_VIRTUAL_TERMINAL_INPUT = 0x200 114 | } 115 | 116 | [Flags] 117 | public enum MouseButtonStates : uint { 118 | FROM_LEFT_1ST_BUTTON_PRESSED = 0x1, 119 | RIGHTMOST_BUTTON_PRESSED = 0x2, 120 | FROM_LEFT_2ND_BUTTON_PRESSED = 0x4, 121 | FROM_LEFT_3RD_BUTTON_PRESSED = 0x8, 122 | FROM_LEFT_4TH_BUTTON_PRESSED = 0x10 123 | } 124 | 125 | [Flags] 126 | public enum MouseEventFlags : uint { 127 | MOUSE_MOVED = 0x1, 128 | DOUBLE_CLICK = 0x2, 129 | MOUSE_WHEELED = 0x4, 130 | MOUSE_HWHEELED = 0x8 131 | } 132 | 133 | [Flags] 134 | public enum PseudoConsoleFlags : uint { 135 | PSEUDOCONSOLE_INHERIT_CURSOR = 0x1 136 | } 137 | 138 | [Flags] 139 | public enum ScreenBufferFlags : uint { 140 | CONSOLE_TEXTMODE_BUFFER = 0x1 141 | } 142 | 143 | [Flags] 144 | public enum ScreenBufferModeFlags : uint { 145 | ENABLE_PROCESSED_OUTPUT = 0x1, 146 | ENABLE_WRAP_AT_EOL_OUTPUT = 0x2, 147 | ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x4, 148 | DISABLE_NEWLINE_AUTO_RETURN = 0x8, 149 | ENABLE_LVB_GRID_WORLDWIDE = 0x10 150 | } 151 | 152 | [Flags] 153 | public enum SelectionInfoFlags : uint { 154 | CONSOLE_NO_SELECTION = 0x0, 155 | CONSOLE_SELECTION_IN_PROGRESS = 0x1, 156 | CONSOLE_SELECTION_NOT_EMPTY = 0x2, 157 | CONSOLE_MOUSE_SELECTION = 0x4, 158 | CONSOLE_MOUSE_DOWN = 0x8 159 | } 160 | 161 | [Flags] 162 | public enum ShareModeFlags : uint { 163 | FILE_SHARE_READ = 0x1, 164 | FILE_SHARE_WRITE = 0x2 165 | } 166 | 167 | public enum StandardDevice : uint { 168 | STD_INPUT_HANDLE = 4294967286, // -10 169 | STD_OUTPUT_HANDLE = 4294967285, // -11 170 | STD_ERROR_HANDLE = 4294967284 // -12 171 | } 172 | 173 | #endregion 174 | 175 | #region Functions 176 | 177 | // Introduced in Windows XP 178 | [DllImport("kernel32.dll", CharSet = CharSet.Unicode, EntryPoint = "AddConsoleAliasW", ExactSpelling = true, SetLastError = true)] 179 | [return: MarshalAs(UnmanagedType.Bool)] 180 | public static extern bool AddConsoleAlias( 181 | [MarshalAs(UnmanagedType.LPWStr)] 182 | string Source, 183 | 184 | [MarshalAs(UnmanagedType.LPWStr)] 185 | string Target, 186 | 187 | [MarshalAs(UnmanagedType.LPWStr)] 188 | string ExeName 189 | ); 190 | 191 | [DllImport("kernel32.dll", SetLastError = true)] 192 | [return: MarshalAs(UnmanagedType.Bool)] 193 | public static extern bool AllocConsole(); 194 | 195 | [DllImport("kernel32.dll", SetLastError = true)] 196 | [return: MarshalAs(UnmanagedType.Bool)] 197 | public static extern bool AttachConsole( 198 | uint dwProcessId 199 | ); 200 | 201 | // Introduced in Windows 10 version 1809 / Windows Server 2019 202 | [DllImport("kernel32.dll")] 203 | public static extern void ClosePseudoConsole( 204 | IntPtr hPC 205 | ); 206 | 207 | [DllImport("kernel32.dll", SetLastError = true)] 208 | public static extern IntPtr CreateConsoleScreenBuffer( 209 | AccessRightsFlags dwDesiredAccess, 210 | ShareModeFlags dwShareMode, 211 | IntPtr lpSecurityAttributes, // TODO 212 | ScreenBufferFlags dwFlags, 213 | IntPtr lpScreenBufferData // NULL 214 | ); 215 | 216 | // Introduced in Windows 10 version 1809 / Windows Server 2019 217 | [DllImport("kernel32.dll")] 218 | public static extern int CreatePseudoConsole( 219 | COORD size, 220 | IntPtr hInput, 221 | IntPtr hOutput, 222 | PseudoConsoleFlags dwFlags, 223 | out IntPtr phPC 224 | ); 225 | 226 | // Present in Windows headers but undocumented 227 | [DllImport("kernel32.dll", CharSet = CharSet.Unicode, EntryPoint = "ExpungeConsoleCommandHistoryW", ExactSpelling = true, SetLastError = true)] 228 | public static extern void ExpungeConsoleCommandHistory( 229 | [MarshalAs(UnmanagedType.LPWStr)] 230 | string ExeName 231 | ); 232 | 233 | [DllImport("kernel32.dll", SetLastError = true)] 234 | [return: MarshalAs(UnmanagedType.Bool)] 235 | public static extern bool FillConsoleOutputAttribute( 236 | IntPtr hConsoleOutput, 237 | CharacterAttributes wAttribute, 238 | uint nLength, 239 | COORD dwWriteCoord, 240 | out uint lpNumberOfAttrsWritten 241 | ); 242 | 243 | [DllImport("kernel32.dll", CharSet = CharSet.Unicode, EntryPoint = "FillConsoleOutputCharacterW", ExactSpelling = true, SetLastError = true)] 244 | [return: MarshalAs(UnmanagedType.Bool)] 245 | public static extern bool FillConsoleOutputCharacter( 246 | IntPtr hConsoleOutput, 247 | char cCharacter, 248 | uint nLength, 249 | COORD dwWriteCoord, 250 | out uint lpNumberOfCharsWritten 251 | ); 252 | 253 | [DllImport("kernel32.dll", SetLastError = true)] 254 | [return: MarshalAs(UnmanagedType.Bool)] 255 | public static extern bool FlushConsoleInputBuffer( 256 | IntPtr hConsoleInput 257 | ); 258 | 259 | [DllImport("kernel32.dll", SetLastError = true)] 260 | [return: MarshalAs(UnmanagedType.Bool)] 261 | public static extern bool FreeConsole(); 262 | 263 | [DllImport("kernel32.dll", SetLastError = true)] 264 | [return: MarshalAs(UnmanagedType.Bool)] 265 | public static extern bool GenerateConsoleCtrlEvent( 266 | ControlEvent dwCtrlEvent, 267 | uint dwProcessGroupId 268 | ); 269 | 270 | // Introduced in Windows XP 271 | [DllImport("kernel32.dll", CharSet = CharSet.Unicode, EntryPoint = "GetConsoleAliasW", ExactSpelling = true, SetLastError = true)] 272 | public static extern uint GetConsoleAlias( 273 | [MarshalAs(UnmanagedType.LPWStr)] 274 | string lpSource, 275 | 276 | [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2)] 277 | out char[] lpTargetBuffer, 278 | 279 | uint TargetBufferLength, 280 | 281 | [MarshalAs(UnmanagedType.LPWStr)] 282 | string lpExeName 283 | ); 284 | 285 | // Introduced in Windows XP 286 | [DllImport("kernel32.dll", CharSet = CharSet.Unicode, EntryPoint = "GetConsoleAliasesW", ExactSpelling = true, SetLastError = true)] 287 | public static extern uint GetConsoleAliases( 288 | [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] 289 | out char[] lpAliasBuffer, 290 | 291 | uint AliasBufferLength, 292 | 293 | [MarshalAs(UnmanagedType.LPWStr)] 294 | string lpExeName 295 | ); 296 | 297 | // Introduced in Windows XP 298 | [DllImport("kernel32.dll", CharSet = CharSet.Unicode, EntryPoint = "GetConsoleAliasesLengthW", ExactSpelling = true)] 299 | public static extern uint GetConsoleAliasesLength( 300 | [MarshalAs(UnmanagedType.LPWStr)] 301 | string lpExeName 302 | ); 303 | 304 | // Introduced in Windows XP 305 | [DllImport("kernel32.dll", CharSet = CharSet.Unicode, EntryPoint = "GetConsoleAliasExesW", ExactSpelling = true, SetLastError = true)] 306 | public static extern uint GetConsoleAliasExes( 307 | [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] 308 | out char[] lpExeNameBuffer, 309 | 310 | uint ExeNameBufferLength 311 | ); 312 | 313 | // Introduced in Windows XP 314 | [DllImport("kernel32.dll", EntryPoint = "GetConsoleAliasExesLengthW")] 315 | public static extern uint GetConsoleAliasExesLength(); 316 | 317 | // Present in Windows headers but undocumented 318 | [DllImport("kernel32.dll", CharSet = CharSet.Unicode, EntryPoint = "GetConsoleCommandHistoryW", ExactSpelling = true, SetLastError = true)] 319 | public static extern uint GetConsoleCommandHistory( 320 | [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] 321 | out char[] Commands, 322 | 323 | uint CommandBufferLength, 324 | 325 | [MarshalAs(UnmanagedType.LPWStr)] 326 | string ExeName 327 | ); 328 | 329 | // Present in Windows headers but undocumented 330 | [DllImport("kernel32.dll", CharSet = CharSet.Unicode, EntryPoint = "GetConsoleCommandHistoryLengthW", ExactSpelling = true, SetLastError = true)] 331 | public static extern uint GetConsoleCommandHistoryLength( 332 | [MarshalAs(UnmanagedType.LPWStr)] 333 | string ExeName 334 | ); 335 | 336 | [DllImport("kernel32.dll", SetLastError = true)] 337 | public static extern uint GetConsoleCP(); 338 | 339 | [DllImport("kernel32.dll", SetLastError = true)] 340 | [return: MarshalAs(UnmanagedType.Bool)] 341 | public static extern bool GetConsoleCursorInfo( 342 | IntPtr hConsoleOutput, 343 | out CONSOLE_CURSOR_INFO lpConsoleCursorInfo 344 | ); 345 | 346 | [DllImport("kernel32.dll", SetLastError = true)] 347 | [return: MarshalAs(UnmanagedType.Bool)] 348 | public static extern bool GetConsoleDisplayMode( 349 | out DisplayModeGetFlags lpModeFlags 350 | ); 351 | 352 | [DllImport("kernel32.dll", SetLastError = true)] 353 | public static extern COORD GetConsoleFontSize( 354 | IntPtr hConsoleOutput, 355 | uint nFont 356 | ); 357 | 358 | [DllImport("kernel32.dll", SetLastError = true)] 359 | [return: MarshalAs(UnmanagedType.Bool)] 360 | public static extern bool GetConsoleHistoryInfo( 361 | out CONSOLE_HISTORY_INFO lpConsoleHistoryInfo 362 | ); 363 | 364 | [DllImport("kernel32.dll", SetLastError = true)] 365 | [return: MarshalAs(UnmanagedType.Bool)] 366 | public static extern bool GetConsoleMode( 367 | IntPtr hConsoleHandle, 368 | out InputModeFlags lpMode 369 | ); 370 | 371 | [DllImport("kernel32.dll", SetLastError = true)] 372 | [return: MarshalAs(UnmanagedType.Bool)] 373 | public static extern bool GetConsoleMode( 374 | IntPtr hConsoleHandle, 375 | out ScreenBufferModeFlags lpMode 376 | ); 377 | 378 | // Introduced in Windows Vista / Windows Server 2008 379 | [DllImport("kernel32.dll", CharSet = CharSet.Unicode, EntryPoint = "GetConsoleOriginalTitleW", ExactSpelling = true, SetLastError = true)] 380 | public static extern uint GetConsoleOriginalTitle( 381 | [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] 382 | out char[] lpConsoleTitle, 383 | 384 | uint nSize 385 | ); 386 | 387 | [DllImport("kernel32.dll", SetLastError = true)] 388 | public static extern uint GetConsoleOutputCP(); 389 | 390 | // Introduced in Windows XP 391 | [DllImport("kernel32.dll", SetLastError = true)] 392 | public static extern uint GetConsoleProcessList( 393 | [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] 394 | out uint[] lpdwProcessList, 395 | 396 | uint dwProcessCount 397 | ); 398 | 399 | [DllImport("kernel32.dll", SetLastError = true)] 400 | [return: MarshalAs(UnmanagedType.Bool)] 401 | public static extern bool GetConsoleScreenBufferInfo( 402 | IntPtr hConsoleOutput, 403 | out CONSOLE_SCREEN_BUFFER_INFO lpConsoleScreenBufferInfo 404 | ); 405 | 406 | [DllImport("kernel32.dll", SetLastError = true)] 407 | [return: MarshalAs(UnmanagedType.Bool)] 408 | public static extern bool GetConsoleScreenBufferInfoEx( 409 | IntPtr hConsoleOutput, 410 | out CONSOLE_SCREEN_BUFFER_INFOEX lpConsoleScreenBufferInfoEx 411 | ); 412 | 413 | [DllImport("kernel32.dll", SetLastError = true)] 414 | [return: MarshalAs(UnmanagedType.Bool)] 415 | public static extern bool GetConsoleSelectionInfo( 416 | out CONSOLE_SELECTION_INFO lpConsoleSelectionInfo 417 | ); 418 | 419 | [DllImport("kernel32.dll", CharSet = CharSet.Unicode, EntryPoint = "GetConsoleTitleW", ExactSpelling = true, SetLastError = true)] 420 | public static extern uint GetConsoleTitle( 421 | [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] 422 | out char[] lpConsoleTitle, 423 | 424 | uint nSize 425 | ); 426 | 427 | [DllImport("kernel32.dll")] 428 | public static extern IntPtr GetConsoleWindow(); 429 | 430 | [DllImport("kernel32.dll", SetLastError = true)] 431 | [return: MarshalAs(UnmanagedType.Bool)] 432 | public static extern bool GetCurrentConsoleFont( 433 | IntPtr hConsoleOutput, 434 | 435 | [MarshalAs(UnmanagedType.Bool)] 436 | bool bMaximumWindow, 437 | 438 | out CONSOLE_FONT_INFO lpConsoleCurrentFont 439 | ); 440 | 441 | [DllImport("kernel32.dll", SetLastError = true)] 442 | [return: MarshalAs(UnmanagedType.Bool)] 443 | public static extern bool GetCurrentConsoleFontEx( 444 | IntPtr hConsoleOutput, 445 | 446 | [MarshalAs(UnmanagedType.Bool)] 447 | bool bMaximumWindow, 448 | 449 | out CONSOLE_FONT_INFOEX lpConsoleCurrentFontEx 450 | ); 451 | 452 | [DllImport("kernel32.dll", SetLastError = true)] 453 | public static extern COORD GetLargestConsoleWindowSize( 454 | IntPtr hConsoleOutput 455 | ); 456 | 457 | [DllImport("kernel32.dll", SetLastError = true)] 458 | [return: MarshalAs(UnmanagedType.Bool)] 459 | public static extern bool GetNumberOfConsoleInputEvents( 460 | IntPtr hConsoleInput, 461 | out uint lpcNumberOfEvents 462 | ); 463 | 464 | [DllImport("kernel32.dll", SetLastError = true)] 465 | [return: MarshalAs(UnmanagedType.Bool)] 466 | public static extern bool GetNumberOfConsoleMouseButtons( 467 | out uint lpNumberOfMouseButtons 468 | ); 469 | 470 | [DllImport("kernel32.dll", SetLastError = true)] 471 | public static extern IntPtr GetStdHandle( 472 | StandardDevice nStdHandle 473 | ); 474 | 475 | [DllImport("kernel32.dll", CharSet = CharSet.Unicode, EntryPoint = "PeekConsoleInputW", ExactSpelling = true, SetLastError = true)] 476 | [return: MarshalAs(UnmanagedType.Bool)] 477 | public static extern bool PeekConsoleInput( 478 | IntPtr hConsoleInput, 479 | 480 | [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2)] 481 | out INPUT_RECORD[] lpBuffer, 482 | 483 | uint nLength, 484 | out uint lpNumberOfEventsRead 485 | ); 486 | 487 | [DllImport("kernel32.dll", CharSet = CharSet.Unicode, EntryPoint = "ReadConsoleW", ExactSpelling = true, SetLastError = true)] 488 | [return: MarshalAs(UnmanagedType.Bool)] 489 | public static extern bool ReadConsole( 490 | IntPtr hConsoleInput, 491 | 492 | [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2)] 493 | out char[] lpBuffer, 494 | 495 | uint nNumberOfCharsToRead, 496 | out uint lpNumberOfCharsRead, 497 | IntPtr pInputControl // NULL 498 | ); 499 | 500 | [DllImport("kernel32.dll", CharSet = CharSet.Unicode, EntryPoint = "ReadConsoleW", ExactSpelling = true, SetLastError = true)] 501 | [return: MarshalAs(UnmanagedType.Bool)] 502 | public static extern bool ReadConsole( 503 | IntPtr hConsoleInput, 504 | 505 | [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2)] 506 | out char[] lpBuffer, 507 | 508 | uint nNumberOfCharsToRead, 509 | out uint lpNumberOfCharsRead, 510 | CONSOLE_READCONSOLE_CONTROL pInputControl 511 | ); 512 | 513 | [DllImport("kernel32.dll", CharSet = CharSet.Unicode, EntryPoint = "ReadConsoleInputW", ExactSpelling = true, SetLastError = true)] 514 | [return: MarshalAs(UnmanagedType.Bool)] 515 | public static extern bool ReadConsoleInput( 516 | IntPtr hConsoleInput, 517 | 518 | [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2)] 519 | out INPUT_RECORD[] lpBuffer, 520 | 521 | uint nLength, 522 | out uint lpNumberOfEventsRead 523 | ); 524 | 525 | [DllImport("kernel32.dll", SetLastError = true)] 526 | [return: MarshalAs(UnmanagedType.Bool)] 527 | public static extern bool ReadConsoleOutputAttribute( 528 | IntPtr hConsoleOutput, 529 | 530 | [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2)] 531 | out CharacterAttributes[] lpAttribute, 532 | 533 | uint nLength, 534 | COORD dwReadCoord, 535 | out uint lpNumberOfAttrsRead 536 | ); 537 | 538 | [DllImport("kernel32.dll", CharSet = CharSet.Unicode, EntryPoint = "ReadConsoleOutputCharacterW", ExactSpelling = true, SetLastError = true)] 539 | [return: MarshalAs(UnmanagedType.Bool)] 540 | public static extern bool ReadConsoleOutputCharacter( 541 | IntPtr hConsoleOutput, 542 | 543 | [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2)] 544 | out char[] lpCharacter, 545 | 546 | uint nLength, 547 | COORD dwReadCoord, 548 | out uint lpNumberOfCharsRead 549 | ); 550 | 551 | // Introduced in Windows 10 version 1809 / Windows Server 2019 552 | [DllImport("kernel32.dll")] 553 | public static extern int ResizePseudoConsole( 554 | IntPtr hPC, 555 | COORD size 556 | ); 557 | 558 | [DllImport("kernel32.dll", CharSet = CharSet.Unicode, EntryPoint = "ScrollConsoleScreenBufferW", ExactSpelling = true, SetLastError = true)] 559 | [return: MarshalAs(UnmanagedType.Bool)] 560 | public static extern bool ScrollConsoleScreenBuffer( 561 | IntPtr hConsoleOutput, 562 | ref SMALL_RECT lpScrollRectangle, 563 | IntPtr lpClipRectangle, // NULL 564 | COORD dwDestinationOrigin, 565 | ref CHAR_INFO lpFill 566 | ); 567 | 568 | [DllImport("kernel32.dll", CharSet = CharSet.Unicode, EntryPoint = "ScrollConsoleScreenBufferW", ExactSpelling = true, SetLastError = true)] 569 | [return: MarshalAs(UnmanagedType.Bool)] 570 | public static extern bool ScrollConsoleScreenBuffer( 571 | IntPtr hConsoleOutput, 572 | ref SMALL_RECT lpScrollRectangle, 573 | ref SMALL_RECT lpClipRectangle, 574 | COORD dwDestinationOrigin, 575 | ref CHAR_INFO lpFill 576 | ); 577 | 578 | [DllImport("kernel32.dll", SetLastError = true)] 579 | [return: MarshalAs(UnmanagedType.Bool)] 580 | public static extern bool SetConsoleActiveScreenBuffer( 581 | IntPtr hConsoleOutput 582 | ); 583 | 584 | [DllImport("kernel32.dll", SetLastError = true)] 585 | [return: MarshalAs(UnmanagedType.Bool)] 586 | public static extern bool SetConsoleCP( 587 | uint wCodePageID 588 | ); 589 | 590 | [DllImport("kernel32.dll", SetLastError = true)] 591 | [return: MarshalAs(UnmanagedType.Bool)] 592 | public static extern bool SetConsoleCtrlHandler( 593 | IntPtr HandlerRoutine, // NULL 594 | 595 | [MarshalAs(UnmanagedType.Bool)] 596 | bool Add 597 | ); 598 | 599 | [DllImport("kernel32.dll", SetLastError = true)] 600 | [return: MarshalAs(UnmanagedType.Bool)] 601 | public static extern bool SetConsoleCtrlHandler( 602 | HandlerRoutine HandlerRoutine, 603 | 604 | [MarshalAs(UnmanagedType.Bool)] 605 | bool Add 606 | ); 607 | 608 | [DllImport("kernel32.dll", SetLastError = true)] 609 | [return: MarshalAs(UnmanagedType.Bool)] 610 | public static extern bool SetConsoleCursorInfo( 611 | IntPtr hConsoleOutput, 612 | ref CONSOLE_CURSOR_INFO lpConsoleCursorInfo 613 | ); 614 | 615 | [DllImport("kernel32.dll", SetLastError = true)] 616 | [return: MarshalAs(UnmanagedType.Bool)] 617 | public static extern bool SetConsoleCursorPosition( 618 | IntPtr hConsoleOutput, 619 | COORD dwCursorPosition 620 | ); 621 | 622 | [DllImport("kernel32.dll", SetLastError = true)] 623 | [return: MarshalAs(UnmanagedType.Bool)] 624 | public static extern bool SetConsoleDisplayMode( 625 | IntPtr hConsoleOutput, 626 | DisplayModeSetFlags dwFlags, 627 | IntPtr lpNewScreenBufferDimensions // NULL 628 | ); 629 | 630 | [DllImport("kernel32.dll", SetLastError = true)] 631 | [return: MarshalAs(UnmanagedType.Bool)] 632 | public static extern bool SetConsoleDisplayMode( 633 | IntPtr hConsoleOutput, 634 | DisplayModeSetFlags dwFlags, 635 | out COORD lpNewScreenBufferDimensions 636 | ); 637 | 638 | [DllImport("kernel32.dll", SetLastError = true)] 639 | [return: MarshalAs(UnmanagedType.Bool)] 640 | public static extern bool SetConsoleHistoryInfo( 641 | CONSOLE_HISTORY_INFO lpConsoleHistoryInfo 642 | ); 643 | 644 | [DllImport("kernel32.dll", SetLastError = true)] 645 | [return: MarshalAs(UnmanagedType.Bool)] 646 | public static extern bool SetConsoleMode( 647 | IntPtr hConsoleHandle, 648 | InputModeFlags dwMode 649 | ); 650 | 651 | [DllImport("kernel32.dll", SetLastError = true)] 652 | [return: MarshalAs(UnmanagedType.Bool)] 653 | public static extern bool SetConsoleMode( 654 | IntPtr hConsoleHandle, 655 | ScreenBufferModeFlags dwMode 656 | ); 657 | 658 | // Present in Windows headers but undocumented 659 | [DllImport("kernel32.dll", CharSet = CharSet.Unicode, EntryPoint = "SetConsoleNumberOfCommandsW", ExactSpelling = true, SetLastError = true)] 660 | [return: MarshalAs(UnmanagedType.Bool)] 661 | public static extern bool SetConsoleNumberOfCommands( 662 | uint Number, 663 | 664 | [MarshalAs(UnmanagedType.LPWStr)] 665 | string ExeName 666 | ); 667 | 668 | [DllImport("kernel32.dll", SetLastError = true)] 669 | [return: MarshalAs(UnmanagedType.Bool)] 670 | public static extern bool SetConsoleOutputCP( 671 | uint wCodePageID 672 | ); 673 | 674 | [DllImport("kernel32.dll", SetLastError = true)] 675 | [return: MarshalAs(UnmanagedType.Bool)] 676 | public static extern bool SetConsoleScreenBufferInfoEx( 677 | IntPtr hConsoleOutput, 678 | CONSOLE_SCREEN_BUFFER_INFOEX lpConsoleScreenBufferInfoEx 679 | ); 680 | 681 | [DllImport("kernel32.dll", SetLastError = true)] 682 | [return: MarshalAs(UnmanagedType.Bool)] 683 | public static extern bool SetConsoleScreenBufferSize( 684 | IntPtr hConsoleOutput, 685 | COORD dwSize 686 | ); 687 | 688 | [DllImport("kernel32.dll", SetLastError = true)] 689 | [return: MarshalAs(UnmanagedType.Bool)] 690 | public static extern bool SetConsoleTextAttribute( 691 | IntPtr hConsoleOutput, 692 | CharacterAttributes wAttributes 693 | ); 694 | 695 | [DllImport("kernel32.dll", CharSet = CharSet.Unicode, EntryPoint = "SetConsoleTitleW", ExactSpelling = true, SetLastError = true)] 696 | [return: MarshalAs(UnmanagedType.Bool)] 697 | public static extern bool SetConsoleTitle( 698 | [MarshalAs(UnmanagedType.LPWStr)] 699 | string lpConsoleTitle 700 | ); 701 | 702 | [DllImport("kernel32.dll", SetLastError = true)] 703 | [return: MarshalAs(UnmanagedType.Bool)] 704 | public static extern bool SetConsoleWindowInfo( 705 | IntPtr hConsoleOutput, 706 | 707 | [MarshalAs(UnmanagedType.Bool)] 708 | bool bAbsolute, 709 | 710 | ref SMALL_RECT lpConsoleWindow 711 | ); 712 | 713 | [DllImport("kernel32.dll", SetLastError = true)] 714 | [return: MarshalAs(UnmanagedType.Bool)] 715 | public static extern bool SetCurrentConsoleFontEx( 716 | IntPtr hConsoleOutput, 717 | 718 | [MarshalAs(UnmanagedType.Bool)] 719 | bool bMaximumWindow, 720 | 721 | CONSOLE_FONT_INFOEX lpConsoleCurrentFontEx 722 | ); 723 | 724 | [DllImport("kernel32.dll", SetLastError = true)] 725 | [return: MarshalAs(UnmanagedType.Bool)] 726 | public static extern bool SetStdHandle( 727 | StandardDevice nStdHandle, 728 | IntPtr hHandle 729 | ); 730 | 731 | [DllImport("kernel32.dll", CharSet = CharSet.Unicode, EntryPoint = "WriteConsoleW", ExactSpelling = true, SetLastError = true)] 732 | [return: MarshalAs(UnmanagedType.Bool)] 733 | public static extern bool WriteConsole( 734 | IntPtr hConsoleOutput, 735 | 736 | [MarshalAs(UnmanagedType.LPWStr)] 737 | string lpBuffer, 738 | 739 | uint nNumberOfCharsToWrite, 740 | IntPtr lpNumberOfCharsWritten, // NULL 741 | IntPtr lpReserved // NULL 742 | ); 743 | 744 | [DllImport("kernel32.dll", CharSet = CharSet.Unicode, EntryPoint = "WriteConsoleW", ExactSpelling = true, SetLastError = true)] 745 | [return: MarshalAs(UnmanagedType.Bool)] 746 | public static extern bool WriteConsole( 747 | IntPtr hConsoleOutput, 748 | 749 | [MarshalAs(UnmanagedType.LPWStr)] 750 | string lpBuffer, 751 | 752 | uint nNumberOfCharsToWrite, 753 | out uint lpNumberOfCharsWritten, 754 | IntPtr lpReserved // NULL 755 | ); 756 | 757 | [DllImport("kernel32.dll", CharSet = CharSet.Unicode, EntryPoint = "WriteConsoleInputW", ExactSpelling = true, SetLastError = true)] 758 | [return: MarshalAs(UnmanagedType.Bool)] 759 | public static extern bool WriteConsoleInput( 760 | IntPtr hConsoleInput, 761 | 762 | [MarshalAs(UnmanagedType.LPArray)] 763 | ref INPUT_RECORD[] lpBuffer, 764 | 765 | uint nLength, 766 | out uint lpNumberOfEventsWritten 767 | ); 768 | 769 | [DllImport("kernel32.dll", SetLastError = true)] 770 | [return: MarshalAs(UnmanagedType.Bool)] 771 | public static extern bool WriteConsoleOutputAttribute( 772 | IntPtr hConsoleOutput, 773 | ref CharacterAttributes lpAttribute, 774 | uint nLength, 775 | COORD dwWriteCoord, 776 | out uint lpNumberOfAttrsWritten 777 | ); 778 | 779 | [DllImport("kernel32.dll", CharSet = CharSet.Unicode, EntryPoint = "WriteConsoleOutputCharacterW", ExactSpelling = true, SetLastError = true)] 780 | [return: MarshalAs(UnmanagedType.Bool)] 781 | public static extern bool WriteConsoleOutputCharacter( 782 | IntPtr hConsoleOutput, 783 | 784 | [MarshalAs(UnmanagedType.LPWStr)] 785 | string lpCharacter, 786 | 787 | uint nLength, 788 | COORD dwWriteCoord, 789 | out uint lpNumberOfCharsWritten 790 | ); 791 | 792 | #endregion 793 | 794 | #region Structures 795 | 796 | [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] 797 | public struct CHAR_INFO { 798 | public char Char; 799 | public CharacterAttributes Attributes; 800 | } 801 | 802 | public struct COLORREF { 803 | public byte R; 804 | public byte G; 805 | public byte B; 806 | } 807 | 808 | public struct CONSOLE_CURSOR_INFO { 809 | public uint dwSize; 810 | public bool bVisible; 811 | } 812 | 813 | public struct CONSOLE_FONT_INFO { 814 | public uint nFont; 815 | public COORD dwFontSize; 816 | } 817 | 818 | [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] 819 | public class CONSOLE_FONT_INFOEX { 820 | public uint cbSize; 821 | public uint nFont; 822 | public COORD dwFontSize; 823 | public uint FontFamily; 824 | public uint FontWeight; 825 | 826 | [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.LPWStr, SizeConst = LF_FACESIZE)] 827 | public char[] FaceName; 828 | 829 | public CONSOLE_FONT_INFOEX() { 830 | cbSize = (uint)Marshal.SizeOf(typeof(CONSOLE_FONT_INFOEX)); 831 | } 832 | } 833 | 834 | [StructLayout(LayoutKind.Sequential)] 835 | public class CONSOLE_HISTORY_INFO { 836 | public uint cbSize; 837 | public uint HistoryBufferSize; 838 | public uint NumberOfHistoryBuffers; 839 | public HistoryInfoFlags dwFlags; 840 | 841 | public CONSOLE_HISTORY_INFO() { 842 | cbSize = (uint)Marshal.SizeOf(typeof(CONSOLE_HISTORY_INFO)); 843 | } 844 | } 845 | 846 | [StructLayout(LayoutKind.Sequential)] 847 | public class CONSOLE_READCONSOLE_CONTROL { 848 | public uint nLength; 849 | public uint nInitialChars; 850 | public uint dwCtrlWakeupMask; 851 | public ControlKeyStates dwControlKeyState; 852 | 853 | public CONSOLE_READCONSOLE_CONTROL() { 854 | nLength = (uint)Marshal.SizeOf(typeof(CONSOLE_READCONSOLE_CONTROL)); 855 | } 856 | } 857 | 858 | public struct CONSOLE_SCREEN_BUFFER_INFO { 859 | public COORD dwSize; 860 | public COORD dwCursorPosition; 861 | public CharacterAttributes wAttributes; 862 | public SMALL_RECT srWindow; 863 | public COORD dwMaximumWindowSize; 864 | } 865 | 866 | [StructLayout(LayoutKind.Sequential)] 867 | public class CONSOLE_SCREEN_BUFFER_INFOEX { 868 | public uint cbSize; 869 | public COORD dwSize; 870 | public COORD dwCursorPosition; 871 | public CharacterAttributes wAttributes; 872 | public SMALL_RECT srWindow; 873 | public COORD dwMaximumWindowSize; 874 | public ushort wPopupAttributes; // TODO 875 | public bool bFullscreenSupported; 876 | 877 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] 878 | public COLORREF[] ColorTable; 879 | 880 | public CONSOLE_SCREEN_BUFFER_INFOEX() { 881 | cbSize = (uint)Marshal.SizeOf(typeof(CONSOLE_SCREEN_BUFFER_INFOEX)); 882 | } 883 | } 884 | 885 | public struct CONSOLE_SELECTION_INFO { 886 | public SelectionInfoFlags dwFlags; 887 | public COORD dwSelectionAnchor; 888 | public SMALL_RECT srSelection; 889 | } 890 | 891 | public struct COORD { 892 | public short X; 893 | public short Y; 894 | } 895 | 896 | public struct FOCUS_EVENT_RECORD { 897 | public bool bSetFocus; 898 | } 899 | 900 | [StructLayout(LayoutKind.Explicit)] 901 | public struct INPUT_RECORD { 902 | [FieldOffset(0)] 903 | public EventType EventType; 904 | 905 | [FieldOffset(4)] 906 | public KEY_EVENT_RECORD KeyEvent; 907 | 908 | [FieldOffset(4)] 909 | public MOUSE_EVENT_RECORD MouseEvent; 910 | 911 | [FieldOffset(4)] 912 | public WINDOW_BUFFER_SIZE_RECORD WindowBufferSizeEvent; 913 | 914 | [FieldOffset(4)] 915 | public MENU_EVENT_RECORD MenuEvent; 916 | 917 | [FieldOffset(4)] 918 | public FOCUS_EVENT_RECORD FocusEvent; 919 | } 920 | 921 | [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] 922 | public struct KEY_EVENT_RECORD { 923 | public bool bKeyDown; 924 | public ushort wRepeatCount; 925 | public ushort wVirtualKeyCode; 926 | public ushort wVirtualScanCode; 927 | public char uChar; 928 | public ControlKeyStates dwControlKeyState; 929 | } 930 | 931 | public struct MENU_EVENT_RECORD { 932 | public uint dwCommandId; 933 | } 934 | 935 | public struct MOUSE_EVENT_RECORD { 936 | public COORD dwMousePosition; 937 | public MouseButtonStates dwButtonState; 938 | public ControlKeyStates dwControlKeyState; 939 | public MouseEventFlags dwEventFlags; 940 | } 941 | 942 | public struct SMALL_RECT { 943 | public short Left; 944 | public short Top; 945 | public short Right; 946 | public short Bottom; 947 | } 948 | 949 | public struct WINDOW_BUFFER_SIZE_RECORD { 950 | public COORD dwSize; 951 | } 952 | 953 | #endregion 954 | } 955 | } 956 | -------------------------------------------------------------------------------- /Native/ConsoleAPI.ps1: -------------------------------------------------------------------------------- 1 | $ConsoleAPI = Get-Content -Raw -Path (Join-Path -Path $PSScriptRoot -ChildPath 'ConsoleAPI.cs') 2 | 3 | if (!('PSWinGlue.ConsoleAPI' -as [Type])) { 4 | Add-Type -TypeDefinition $ConsoleAPI 5 | } else { 6 | Write-Warning -Message 'Unable to add ConsoleAPI type as it already exists.' 7 | } 8 | -------------------------------------------------------------------------------- /PSScriptAnalyzerSettings.psd1: -------------------------------------------------------------------------------- 1 | # PSScriptAnalyzer settings 2 | # 3 | # Last reviewed release: v1.22.0 4 | 5 | @{ 6 | IncludeRules = @('*') 7 | 8 | ExcludeRules = @( 9 | 'PSReviewUnusedParameter' 10 | ) 11 | 12 | Rules = @{ 13 | # Compatibility rules 14 | PSUseCompatibleSyntax = @{ 15 | Enable = $true 16 | # Only major versions from v3.0 are supported 17 | TargetVersions = @('3.0', '4.0', '5.0', '6.0', '7.0') 18 | } 19 | 20 | # General rules 21 | PSAlignAssignmentStatement = @{ 22 | Enable = $true 23 | CheckHashtable = $true 24 | } 25 | 26 | PSAvoidUsingPositionalParameters = @{ 27 | Enable = $true 28 | CommandAllowList = @() 29 | } 30 | 31 | PSPlaceCloseBrace = @{ 32 | Enable = $true 33 | IgnoreOneLineBlock = $true 34 | NewLineAfter = $false 35 | NoEmptyLineBefore = $false 36 | } 37 | 38 | PSPlaceOpenBrace = @{ 39 | Enable = $true 40 | IgnoreOneLineBlock = $true 41 | NewLineAfter = $true 42 | OnSameLine = $true 43 | } 44 | 45 | PSProvideCommentHelp = @{ 46 | Enable = $true 47 | BlockComment = $true 48 | ExportedOnly = $true 49 | Placement = 'begin' 50 | VSCodeSnippetCorrection = $false 51 | } 52 | 53 | PSUseConsistentIndentation = @{ 54 | Enable = $true 55 | IndentationSize = 4 56 | Kind = 'space' 57 | PipelineIndentation = 'IncreaseIndentationForFirstPipeline' 58 | } 59 | 60 | PSUseConsistentWhitespace = @{ 61 | Enable = $true 62 | CheckInnerBrace = $true 63 | CheckOpenBrace = $true 64 | CheckOpenParen = $true 65 | CheckOperator = $true 66 | CheckParameter = $true 67 | CheckPipe = $true 68 | CheckPipeForRedundantWhitespace = $true 69 | CheckSeparator = $true 70 | IgnoreAssignmentOperatorInsideHashTable = $true 71 | } 72 | 73 | PSUseSingularNouns = @{ 74 | Enable = $true 75 | # If unset, defaults to: Data, Windows 76 | NounAllowList = @() 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /PSWinGlue.psd1: -------------------------------------------------------------------------------- 1 | # 2 | # Module manifest for module 'PSWinGlue' 3 | # 4 | 5 | @{ 6 | 7 | # Script module or binary module file associated with this manifest. 8 | RootModule = 'PSWinGlue.psm1' 9 | 10 | # Version number of this module. 11 | ModuleVersion = '0.6.11' 12 | 13 | # Supported PSEditions 14 | # CompatiblePSEditions = @() 15 | 16 | # ID used to uniquely identify this module 17 | GUID = '1cf38716-9a00-46c5-b9f7-2e50a51cf8b1' 18 | 19 | # Author of this module 20 | Author = 'Samuel Leslie' 21 | 22 | # Company or vendor of this module 23 | # CompanyName = '' 24 | 25 | # Copyright statement for this module 26 | Copyright = '(c) Samuel Leslie. All rights reserved.' 27 | 28 | # Description of the functionality provided by this module 29 | Description = 'An assortment of useful PowerShell scripts' 30 | 31 | # Minimum version of the PowerShell engine required by this module 32 | PowerShellVersion = '3.0' 33 | 34 | # Name of the PowerShell host required by this module 35 | # PowerShellHostName = '' 36 | 37 | # Minimum version of the PowerShell host required by this module 38 | # PowerShellHostVersion = '' 39 | 40 | # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 41 | # DotNetFrameworkVersion = '' 42 | 43 | # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 44 | # ClrVersion = '' 45 | 46 | # Processor architecture (None, X86, Amd64) required by this module 47 | # ProcessorArchitecture = '' 48 | 49 | # Modules that must be imported into the global environment prior to importing this module 50 | # RequiredModules = @() 51 | 52 | # Assemblies that must be loaded prior to importing this module 53 | # RequiredAssemblies = @() 54 | 55 | # Script files (.ps1) that are run in the caller's environment prior to importing this module. 56 | # ScriptsToProcess = @() 57 | 58 | # Type files (.ps1xml) to be loaded when importing this module 59 | # TypesToProcess = @() 60 | 61 | # Format files (.ps1xml) to be loaded when importing this module 62 | # FormatsToProcess = @() 63 | 64 | # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess 65 | # NestedModules = @() 66 | 67 | # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. 68 | FunctionsToExport = @( 69 | 'Add-VpnCspConnection', 70 | 'Find-OrphanDependencyPackages', 71 | 'Get-ControlledGpoStatus', 72 | 'Get-Fonts', 73 | 'Get-InstalledPrograms', 74 | 'Get-TaskSchedulerEvent', 75 | 'Hide-SilverlightUpdates', 76 | 'Install-ExcelAddin', 77 | 'Install-Font', 78 | 'Install-VSTOAddin', 79 | 'Register-MicrosoftUpdate', 80 | 'Remove-AlternateDataStream', 81 | 'Remove-OrphanDependencyPackages', 82 | 'Set-SharedPCMode', 83 | 'Sort-RegistryExport', 84 | 'Uninstall-Font', 85 | 'Uninstall-ObsoleteModule', 86 | 'Uninstall-VSTOAddin', 87 | 'Update-GitRepository', 88 | 'Update-OneDriveSetup' 89 | ) 90 | 91 | # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. 92 | CmdletsToExport = @() 93 | 94 | # Variables to export from this module 95 | VariablesToExport = '*' 96 | 97 | # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. 98 | AliasesToExport = @() 99 | 100 | # DSC resources to export from this module 101 | # DscResourcesToExport = @() 102 | 103 | # List of all modules packaged with this module 104 | # ModuleList = @() 105 | 106 | # List of all files packaged with this module 107 | # FileList = @() 108 | 109 | # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. 110 | PrivateData = @{ 111 | 112 | PSData = @{ 113 | 114 | # Tags applied to this module. These help with module discovery in online galleries. 115 | Tags = @( 116 | 'scripts', 'sysadmin' 117 | 'Windows', 118 | 'PSEdition_Desktop' 119 | ) 120 | 121 | # A URL to the license for this module. 122 | LicenseUri = 'https://github.com/ralish/PSWinGlue/blob/stable/LICENSE' 123 | 124 | # A URL to the main website for this project. 125 | ProjectUri = 'https://github.com/ralish/PSWinGlue' 126 | 127 | # A URL to an icon representing this module. 128 | # IconUri = '' 129 | 130 | # ReleaseNotes of this module 131 | ReleaseNotes = 'https://github.com/ralish/PSWinGlue/blob/stable/CHANGELOG.md' 132 | 133 | # Prerelease string of this module 134 | # Prerelease = '' 135 | 136 | # Flag to indicate whether the module requires explicit user acceptance for install/update/save 137 | # RequireLicenseAcceptance = $false 138 | 139 | # External dependent modules of this module 140 | # ExternalModuleDependencies = @() 141 | 142 | } # End of PSData hashtable 143 | 144 | } # End of PrivateData hashtable 145 | 146 | # HelpInfo URI of this module 147 | # HelpInfoURI = '' 148 | 149 | # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. 150 | # DefaultCommandPrefix = '' 151 | 152 | } 153 | -------------------------------------------------------------------------------- /PSWinGlue.psm1: -------------------------------------------------------------------------------- 1 | # See the help for Set-StrictMode for the full details on what this enables. 2 | Set-StrictMode -Version 2.0 3 | 4 | # Import all scripts as functions 5 | $Scripts = Get-ChildItem -Path (Join-Path -Path $PSScriptRoot -ChildPath 'Scripts') -File 6 | foreach ($Script in $Scripts) { 7 | $FunctionName = $Script.BaseName 8 | $FunctionPath = 'Function:\{0}' -f $FunctionName 9 | 10 | if (Test-Path -Path $FunctionPath) { 11 | Write-Warning -Message ('Skipping import of existing function: {0}' -f $FunctionName) 12 | continue 13 | } 14 | 15 | New-Item -Path $FunctionPath -Value (Get-Content -Path $Script.FullName -Raw) 16 | } 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PSWinGlue 2 | ========= 3 | 4 | [![pwsh ver](https://img.shields.io/powershellgallery/v/PSWinGlue)](https://www.powershellgallery.com/packages/PSWinGlue) 5 | [![pwsh dl](https://img.shields.io/powershellgallery/dt/PSWinGlue)](https://www.powershellgallery.com/packages/PSWinGlue) 6 | [![license](https://img.shields.io/github/license/ralish/PSWinGlue)](https://choosealicense.com/licenses/mit/) 7 | 8 | A PowerShell module consisting of an assortment of useful scripts. 9 | 10 | - [Requirements](#requirements) 11 | - [Installing](#installing) 12 | - [Usage](#usage) 13 | - [Functions](#functions) 14 | - [License](#license) 15 | 16 | Requirements 17 | ------------ 18 | 19 | - PowerShell 3.0 (or later) 20 | Some functions require a later PowerShell version 21 | 22 | Installing 23 | ---------- 24 | 25 | ### PowerShellGet (included with PowerShell 5.0) 26 | 27 | The module is published to the [PowerShell Gallery](https://www.powershellgallery.com/packages/PSWinGlue): 28 | 29 | ```posh 30 | Install-Module -Name PSWinGlue 31 | ``` 32 | 33 | ### ZIP File 34 | 35 | Download the [ZIP file](https://github.com/ralish/PSWinGlue/archive/stable.zip) of the latest release and unpack it to one of the following locations: 36 | 37 | - Current user: `C:\Users\\Documents\WindowsPowerShell\Modules\PSWinGlue` 38 | - All users: `C:\Program Files\WindowsPowerShell\Modules\PSWinGlue` 39 | 40 | ### Git Clone 41 | 42 | You can also clone the repository into one of the above locations if you'd like the ability to easily update it via Git. 43 | 44 | ### Did it work? 45 | 46 | You can check that PowerShell is able to locate the module by running the following at a PowerShell prompt: 47 | 48 | ```posh 49 | Get-Module PSWinGlue -ListAvailable 50 | ``` 51 | 52 | Usage 53 | ----- 54 | 55 | This module has been written to support two methods of using the functions it includes: 56 | 57 | - As a regular PowerShell module (i.e. install module and call the commands it exports) 58 | - Calling individual functions directly via their script file independent of the module 59 | 60 | To support the latter, each exported function resides in its own script file and has no dependencies on code elsewhere in the module. This allows for easy usage of the functions in scenarios where it may not be desirable to install the entire module (e.g. logon scripts or in other automated contexts). A given script can simply be copied to the desired location and directly called, typically with no additional setup beyond any documented external dependencies. 61 | 62 | Functions 63 | --------- 64 | 65 | ### Add-VpnCspConnection 66 | 67 | Adds a VPN connection using the VPNv2 CSP via the MDM Bridge WMI Provider. 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 |
Supported OS(s)Windows 10 1607 or later
Minimum PowerShell version5.1
Required 3rd-party module(s)None
Required 3rd-party softwareNone
87 | 88 | ### Find-OrphanDependencyPackages 89 | 90 | Locates orphan dependency packages in the system package cache. 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 |
Supported OS(s)Windows
Minimum PowerShell version3.0
Required 3rd-party module(s)None
Required 3rd-party softwareNone
110 | 111 | ### Get-ControlledGpoStatus 112 | 113 | Check Windows domain GPOs and AGPM server controlled GPOs are in sync. 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 |
Supported OS(s)Windows
Minimum PowerShell version3.0
Required 3rd-party module(s)GroupPolicy
Microsoft.Agpm
Required 3rd-party softwareNone
133 | 134 | ### Get-Fonts 135 | 136 | Retrieves registered fonts. 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 |
Supported OS(s)Windows
Minimum PowerShell version3.0
Required 3rd-party module(s)None
Required 3rd-party softwareNone
156 | 157 | ### Get-InstalledPrograms 158 | 159 | Retrieves installed programs. 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 |
Supported OS(s)Windows
Minimum PowerShell version3.0
Required 3rd-party module(s)None
Required 3rd-party softwareNone
179 | 180 | ### Get-TaskSchedulerEvent 181 | 182 | Retrieves events matching the specified IDs from the Task Scheduler event log. 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 |
Supported OS(s)Windows
Minimum PowerShell version3.0
Required 3rd-party module(s)None
Required 3rd-party softwareNone
202 | 203 | ### Hide-SilverlightUpdates 204 | 205 | Hides Silverlight updates. 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 |
Supported OS(s)Windows
Minimum PowerShell version3.0
Required 3rd-party module(s)None
Required 3rd-party softwareNone
225 | 226 | ### Install-ExcelAddin 227 | 228 | Installs Excel add-ins. 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 |
Supported OS(s)Windows
Minimum PowerShell version3.0
Required 3rd-party module(s)None
Required 3rd-party softwareMicrosoft Excel
248 | 249 | ### Install-Font 250 | 251 | Installs a specific font or all fonts from a directory. 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 |
Supported OS(s)Windows
Minimum PowerShell version4.0
Required 3rd-party module(s)None
Required 3rd-party softwareNone
271 | 272 | ### Install-VSTOAddin 273 | 274 | Install a Visual Studio Tools for Office (VSTO) add-in. 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 |
Supported OS(s)Windows
Minimum PowerShell version3.0
Required 3rd-party module(s)None
Required 3rd-party softwareMicrosoft Visual Studio Tools for Office (VSTO) Runtime
294 | 295 | ### Register-MicrosoftUpdate 296 | 297 | Register the Microsoft Update service with the Windows Update Agent. 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 |
Supported OS(s)Windows
Minimum PowerShell version3.0
Required 3rd-party module(s)None
Required 3rd-party softwareNone
317 | 318 | ### Remove-AlternateDataStream 319 | 320 | Remove common unwanted alternate data streams from files. 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 |
Supported OS(s)Windows
Minimum PowerShell version3.0
Required 3rd-party module(s)None
Required 3rd-party softwareNone
340 | 341 | ### Remove-OrphanDependencyPackages 342 | 343 | Removes orphan dependency packages in the system package cache. 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 |
Supported OS(s)Windows
Minimum PowerShell version3.0
Required 3rd-party module(s)None
Required 3rd-party softwareNone
363 | 364 | ### Set-SharedPCMode 365 | 366 | Configures Shared PC Mode using the SharedPC CSP via the MDM Bridge WMI Provider. 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 |
Supported OS(s)Windows 10 1607 or later
Minimum PowerShell version5.1
Required 3rd-party module(s)None
Required 3rd-party softwareNone
386 | 387 | ### Sort-RegistryExport 388 | 389 | Lexically sorts the exported values for each registry key in a Windows Registry export. 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 |
Supported OS(s)Windows
Minimum PowerShell version3.0
Required 3rd-party module(s)None
Required 3rd-party softwareNone
409 | 410 | ### Uninstall-Font 411 | 412 | Uninstalls a specific font by name. 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 |
Supported OS(s)Windows
Minimum PowerShell version3.0
Required 3rd-party module(s)None
Required 3rd-party softwareNone
432 | 433 | ### Uninstall-ObsoleteModule 434 | 435 | Uninstalls obsolete PowerShell modules. 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 |
Supported OS(s)Linux, macOS, Windows
Minimum PowerShell version3.0
Required 3rd-party module(s)PowerShellGet
Required 3rd-party softwareNone
455 | 456 | ### Uninstall-VSTOAddin 457 | 458 | Uninstall a Visual Studio Tools for Office (VSTO) add-in. 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 |
Supported OS(s)Windows
Minimum PowerShell version3.0
Required 3rd-party module(s)None
Required 3rd-party softwareMicrosoft Visual Studio Tools for Office (VSTO) Runtime
478 | 479 | ### Update-GitRepository 480 | 481 | Updates a Git repository. 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 |
Supported OS(s)Windows
Minimum PowerShell version3.0
Required 3rd-party module(s)None
Required 3rd-party softwareGit
501 | 502 | ### Update-OneDriveSetup 503 | 504 | Update OneDrive during Windows image creation. 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 |
Supported OS(s)Windows
Minimum PowerShell version3.0
Required 3rd-party module(s)None
Required 3rd-party softwareMicrosoft OneDrive
524 | 525 | License 526 | ------- 527 | 528 | All content is licensed under the terms of [The MIT License](LICENSE). 529 | -------------------------------------------------------------------------------- /Scripts/Add-VpnCspConnection.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Adds a VPN connection using the VPNv2 CSP via the MDM Bridge WMI Provider 4 | 5 | .DESCRIPTION 6 | Uses the MDM Bridge WMI Provider to interact with the MDM_VPNv2_01 class for adding VPNv2 connections. 7 | 8 | .PARAMETER ProfileName 9 | The profile name for the VPN connection. 10 | 11 | .PARAMETER ProfileXml 12 | The ProfileXML specifying the settings for the VPN connection. 13 | 14 | .PARAMETER ProfilePath 15 | The path to the file containing the ProfileXML specifying the settings for the VPN connection. 16 | 17 | .PARAMETER PassThru 18 | Return the created WMI instance corresponding to the VPN profile. 19 | 20 | .EXAMPLE 21 | Add-VpnCspConnection -ProfileName 'My VPN' -ProfilePath 'D:\My VPN.xml' 22 | 23 | Creates a new VPN profile named "My VPN" using the ProfileXML in the "D:\My VPN.xml" file. 24 | 25 | .NOTES 26 | VPN configuration using the VPNv2 CSP is only available on Windows 10 1607 or later. 27 | 28 | To interact with the MDM Bridge WMI Provider the function must be running as SYSTEM. 29 | 30 | Typically this function would be run non-interactively by a service running in the SYSTEM context (e.g. Group Policy Client). 31 | 32 | To run this function interactively you should use a tool like Sysinternals PsExec to run it under the SYSTEM account. 33 | 34 | For example, the following PsExec command will launch PowerShell under the SYSTEM account: psexec -s -i powershell 35 | 36 | VPNv2 CSP 37 | https://learn.microsoft.com/en-us/windows/client-management/mdm/vpnv2-csp 38 | 39 | MDM_VPNv2_01 class 40 | https://learn.microsoft.com/en-us/windows/win32/dmwmibridgeprov/mdm-vpnv2-01 41 | 42 | .LINK 43 | https://github.com/ralish/PSWinGlue 44 | #> 45 | 46 | # Minimum supported Windows release ships with PowerShell 5.1 47 | #Requires -Version 5.1 48 | 49 | [CmdletBinding()] 50 | [OutputType([Void], [Microsoft.Management.Infrastructure.CimInstance])] 51 | Param( 52 | [Parameter(Mandatory)] 53 | [String]$ProfileName, 54 | 55 | [Parameter(ParameterSetName = 'Path', Mandatory)] 56 | [String]$ProfilePath, 57 | 58 | [Parameter(ParameterSetName = 'Xml', Mandatory)] 59 | [Xml]$ProfileXml, 60 | 61 | [Switch]$PassThru 62 | ) 63 | 64 | $WmiNamespace = 'root\cimv2\mdm\dmmap' 65 | $WmiClassName = 'MDM_VPNv2_01' 66 | $MdmCspPath = './Vendor/MSFT/VPNv2' 67 | 68 | $OSRequiredType = 1 # Workstation 69 | $OSRequiredBuild = 14393 # Windows 10 1607 70 | $SidSystem = 'S-1-5-18' # NT AUTHORITY\SYSTEM 71 | 72 | $PowerShellCore = New-Object -TypeName Version -ArgumentList 6, 0 73 | if ($PSVersionTable.PSVersion -ge $PowerShellCore -and $PSVersionTable.Platform -ne 'Win32NT') { 74 | throw '{0} is only compatible with Windows.' -f $MyInvocation.MyCommand.Name 75 | } 76 | 77 | $OSCurrentType = (Get-CimInstance -ClassName 'Win32_OperatingSystem' -Verbose:$false).ProductType 78 | $OSCurrentBuild = [Environment]::OSVersion.Version.Build 79 | if ($OSCurrentBuild -lt $OSRequiredBuild -or $OSCurrentType -ne $OSRequiredType) { 80 | throw 'VPN configuration with ProfileXML is only available on Windows 10 1607 or later.' 81 | } 82 | 83 | $PowerShellMin = New-Object -TypeName Version -ArgumentList 5, 1 84 | if ($PSVersionTable.PSVersion -lt $PowerShellMin) { 85 | throw '{0} requires at least PowerShell {1}.' -f $MyInvocation.MyCommand.Name, $PowerShellMin 86 | } 87 | 88 | $SidCurrent = ([Security.Principal.WindowsIdentity]::GetCurrent()).User.Value 89 | if ($SidCurrent -ne $SidSystem) { 90 | throw 'Must be running as SYSTEM to interact with MDM Bridge WMI Provider.' 91 | } 92 | 93 | if ($PSCmdlet.ParameterSetName -eq 'Path') { 94 | try { 95 | $ProfileXml = [Xml](Get-Content -Path $ProfilePath -Raw -ErrorAction Stop) 96 | } catch { 97 | throw $_ 98 | } 99 | } 100 | 101 | $MdmCspProperties = @{ 102 | ParentID = $MdmCspPath 103 | InstanceID = [Uri]::EscapeDataString($ProfileName) 104 | ProfileXML = [Security.SecurityElement]::Escape($ProfileXml.InnerXml) 105 | } 106 | 107 | $VpnProfile = New-CimInstance -Namespace $WmiNamespace -ClassName $WmiClassName -Property $MdmCspProperties -ErrorAction Stop 108 | 109 | if ($PassThru) { 110 | return $VpnProfile 111 | } 112 | -------------------------------------------------------------------------------- /Scripts/Find-OrphanDependencyPackages.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Locates orphan dependency packages in the system package cache 4 | 5 | .DESCRIPTION 6 | Windows maintains a local package cache of installed software to simplify operations which require access to the original installer. The package cache is typically located at "C:\ProgramData\Package Cache". 7 | 8 | Not all installers use the package cache, but Windows Installer (MSI files) typically do, as without a cached copy of the installer it's not possible to modify or even remove the existing installation. 9 | 10 | Unfortunately, the package cache may in some cases maintain installers for removed applications, growing in size as old installers are accumulated. This is especially the case for dependency packages. 11 | 12 | Visual Studio is an particularly prominent offender, as it relies on many MSIs which are frequently updated, but not always cleanly removed. The various .NET Core packages are the most common examples. 13 | 14 | This function attempts to identify "orphaned" dependency packages, which after inspection can be removed using the Remove-OrphanDependencyPackages function. You use this function entirely at your own risk! 15 | 16 | .EXAMPLE 17 | Find-OrphanDependencyPackages 18 | 19 | Analyzes the registry and file system for orphan dependency packages. 20 | 21 | .NOTES 22 | There's no simple way to "clean" the package cache and associated registry data. The best that can be done is to try and determine if a package is unused and match registry data to a cached installer. 23 | 24 | The general process is to inspect the registry data for each package, and given an absence of any metadata and a "Dependents" key with no sub-keys, it's fairly safe to assume it is an orphaned dependency. 25 | 26 | Matching a package to a cached installer is non-trivial, as there's no general way to make the match. This function is able to do so for various .NET Core packages due to the predictable naming scheme. 27 | 28 | .LINK 29 | https://github.com/ralish/PSWinGlue 30 | #> 31 | 32 | #Requires -Version 3.0 33 | 34 | [CmdletBinding()] 35 | [OutputType([PSCustomObject[]])] 36 | Param() 37 | 38 | $PowerShellCore = New-Object -TypeName Version -ArgumentList 6, 0 39 | if ($PSVersionTable.PSVersion -ge $PowerShellCore -and $PSVersionTable.Platform -ne 'Win32NT') { 40 | throw '{0} is only compatible with Windows.' -f $MyInvocation.MyCommand.Name 41 | } 42 | 43 | # System package cache 44 | $Script:SysPackageCache = Join-Path -Path $env:ProgramData -ChildPath 'Package Cache' 45 | # Visual Studio package cache 46 | $Script:VsPackageCache = Join-Path -Path $env:ProgramData -ChildPath 'Microsoft\VisualStudio\Packages' 47 | 48 | # Registry MSI metadata 49 | $Script:InstallerRegPath = 'HKLM:\SOFTWARE\Classes\Installer' 50 | # Registry MSI dependencies 51 | $Script:DependenciesRegPath = Join-Path -Path $InstallerRegPath -ChildPath 'Dependencies' 52 | 53 | # Known packages 54 | $Script:KnownPackages = @{ 55 | 'dotnet_apphost_pack' = @{ 56 | Registry = 'dotnet_apphost_pack_(\d+\.\d+\.\d+)_([a-z0-9_]+)' 57 | Directory = 'v$1' 58 | File = '^dotnet-apphost-pack-.+-$2\.msi' 59 | } 60 | # The registry key name is insufficient to match to MSI files in the 61 | # package cache. Instead, we have to find MSI files matching the below 62 | # pattern, then extract a record from them which we can use to match 63 | # against the correct registry key. 64 | #'Dotnet_CLI' = @{ 65 | # Registry = 'Dotnet_CLI_(\d+\.\d+\.\d+)\.\d+_([a-z0-9]+)' 66 | # Directory = 'v\d+\.\d+\.\d+' 67 | # File = '^dotnet-sdk-internal-.+-$2\.msi' 68 | #} 69 | 'Dotnet_CLI_HostFxr' = @{ 70 | Registry = 'Dotnet_CLI_HostFxr_(\d+\.\d+\.\d+)_([a-z0-9]+)' 71 | Directory = 'v$1' 72 | File = '^dotnet-hostfxr-.+-$2\.msi' 73 | } 74 | 'Dotnet_CLI_SharedHost' = @{ 75 | Registry = 'Dotnet_CLI_SharedHost_(\d+\.\d+(\.\d+)?)_([a-z0-9]+)' 76 | Directory = 'v$1' 77 | File = '^dotnet-host-.+-$2\.msi' 78 | } 79 | 'dotnet_runtime' = @{ 80 | Registry = 'dotnet_runtime_(\d+\.\d+\.\d+)_([a-z0-9]+)' 81 | Directory = 'v$1' 82 | File = '^dotnet-runtime-.+-$2\.msi' 83 | } 84 | 'dotnet_targeting_pack' = @{ 85 | Registry = 'dotnet_targeting_pack_(\d+\.\d+\.\d+)_([a-z0-9]+)' 86 | Directory = 'v$1' 87 | File = '^dotnet-targeting-pack-.+-$2\.msi' 88 | } 89 | 'DotNet.CLI.SharedFramework.Microsoft.NETCore.App' = @{ 90 | Registry = 'DotNet\.CLI\.SharedFramework\.Microsoft\.NETCore\.App_(\d+\.\d+\.\d+)_([a-z0-9]+)' 91 | Directory = 'v\d+\.\d+\.\d+' 92 | File = '^dotnet-runtime-$1-.+-$2\.msi' 93 | } 94 | 'Microsoft.AspNetCore.SharedFramework' = @{ 95 | Registry = 'Microsoft\.AspNetCore\.SharedFramework_([a-z0-9]+)_.+,v(\d+\.\d+\.\d+)' 96 | Directory = 'v$2' 97 | File = '^AspNetCoreSharedFramework-$1\.msi' 98 | } 99 | 'Microsoft.AspNetCore.TargetingPack' = @{ 100 | Registry = 'Microsoft\.AspNetCore\.TargetingPack_([a-z0-9]+)_.+,v(\d+\.\d+\.\d+)' 101 | Directory = 'v$2' 102 | File = '^aspnetcore-targeting-pack-$2-.+-$1\.msi' 103 | } 104 | 'NetCore_Templates' = @{ 105 | Registry = 'NetCore_Templates_\d+\.\d+_(\d+\.\d+\.\d+).*_([a-z0-9]+)' 106 | Directory = 'v$1' 107 | File = '^dotnet-\d+templates-.+-$2\.msi' 108 | } 109 | 'windowsdesktop_runtime' = @{ 110 | Registry = 'windowsdesktop_runtime_(\d+\.\d+\.\d+)_([a-z0-9]+)' 111 | Directory = 'v$1' 112 | File = '^windowsdesktop-runtime-.+-$2\.msi' 113 | } 114 | 'windowsdesktop_targeting_pack' = @{ 115 | Registry = 'windowsdesktop_targeting_pack_(\d+\.\d+\.\d+)_([a-z0-9]+)' 116 | Directory = 'v$1' 117 | File = '^windowsdesktop-targeting-pack-.+-$2\.msi' 118 | } 119 | } 120 | 121 | Function Find-DotNetCliPackagesFromCache { 122 | [CmdletBinding()] 123 | [OutputType([Object[]])] 124 | Param() 125 | 126 | $Results = New-Object -TypeName 'Collections.Generic.List[PSCustomObject]' 127 | 128 | # Retrieve all Dotnet_CLI packages 129 | $DncFileRegex = '^dotnet-sdk-internal-.+\.msi' 130 | $DncInstallers = @(Get-ChildItem -Path $Script:PackageCaches -File -Recurse | Where-Object Name -Match $DncFileRegex) 131 | 132 | if ($DncInstallers.Count -eq 0) { 133 | Write-Verbose -Message 'No .NET CLI packages found in package caches.' 134 | return , $Results.ToArray() 135 | } 136 | 137 | # Windows Installer object for querying MSI databases 138 | $Msi = New-Object -ComObject 'WindowsInstaller.Installer' 139 | 140 | # Windows Installer method parameters 141 | $MsiOpenDatabaseModeReadOnly = 0 142 | $MsiOpenViewQuery = @('SELECT `ProviderKey` FROM `WixDependencyProvider`') 143 | 144 | foreach ($DncInstaller in $DncInstallers) { 145 | $DotNetCli = [PSCustomObject]@{ 146 | Name = $DncInstaller.Name 147 | File = $DncInstaller 148 | Provider = [String]::Empty 149 | } 150 | 151 | $Results.Add($DotNetCli) 152 | 153 | try { 154 | # Open the MSI database 155 | $MsiOpenDatabaseParams = $DncInstaller.FullName, $MsiOpenDatabaseModeReadOnly 156 | $MsiDatabase = $Msi.GetType().InvokeMember('OpenDatabase', 'InvokeMethod', $null, $Msi, $MsiOpenDatabaseParams) 157 | 158 | # Retrieve all records from the WixDependencyProvider table. Only a 159 | # subset of SQL is supported and the "LIKE" clause is unfortunately 160 | # not included. 161 | $MsiView = $Msi.GetType().InvokeMember('OpenView', 'InvokeMethod', $null, $MsiDatabase, $MsiOpenViewQuery) 162 | $null = $MsiView.GetType().InvokeMember('Execute', 'InvokeMethod', $null, $MsiView, $null) 163 | 164 | # Iterate over the returned records (there should only be one) 165 | while ($MsiRecord = $MsiView.GetType().InvokeMember('Fetch', 'InvokeMethod', $null, $MsiView, $null)) { 166 | $MsiRecordValue = $MsiRecord.GetType().InvokeMember('StringData', 'GetProperty', $null, $MsiRecord, 1) 167 | if ($MsiRecordValue -match '^Dotnet_CLI_') { 168 | $DotNetCli.Provider = $MsiRecordValue 169 | break 170 | } 171 | } 172 | } finally { 173 | $null = [Runtime.InteropServices.Marshal]::ReleaseComObject($MsiRecord) 174 | $null = [Runtime.InteropServices.Marshal]::ReleaseComObject($MsiView) 175 | $null = [Runtime.InteropServices.Marshal]::ReleaseComObject($MsiDatabase) 176 | } 177 | 178 | if (!$DotNetCli.Provider) { 179 | Write-Warning -Message ('[{0}] Failed to find Provider value.' -f $DncInstaller.Name) 180 | } 181 | } 182 | 183 | $null = [Runtime.InteropServices.Marshal]::ReleaseComObject($Msi) 184 | 185 | return ($Results.ToArray() | Sort-Object -Property Name) 186 | } 187 | 188 | Function Find-OrphanDependenciesFromRegistry { 189 | [CmdletBinding()] 190 | [OutputType([Object[]])] 191 | Param() 192 | 193 | $Results = New-Object -TypeName 'Collections.Generic.List[PSCustomObject]' 194 | 195 | # Retrieve all package dependencies 196 | $Dependencies = @(Get-ChildItem -Path $Script:DependenciesRegPath) 197 | 198 | if ($Dependencies.Count -eq 0) { 199 | Write-Warning -Message 'No MSI dependency packages found in the registry.' 200 | return , $Results.ToArray() 201 | } 202 | 203 | # Filter out packages with dependencies 204 | foreach ($DependencyKey in $Dependencies) { 205 | $Dependency = [PSCustomObject]@{ 206 | Name = $DependencyKey.PSChildName 207 | Status = 'Active' 208 | RegKey = $DependencyKey -replace '^HKEY_LOCAL_MACHINE', 'HKLM:' 209 | CacheStatus = $null 210 | CacheFiles = (New-Object -TypeName 'Collections.Generic.List[IO.FileSystemInfo]') 211 | } 212 | 213 | $Results.Add($Dependency) 214 | 215 | # Any values (inc. default value) 216 | $BaseValues = Get-ItemProperty -Path $DependencyKey.PSPath 217 | if ($BaseValues) { 218 | continue 219 | } 220 | 221 | # No sub-keys 222 | $BaseKeys = @(Get-ChildItem -Path $DependencyKey.PSPath) 223 | if (!$BaseKeys) { 224 | continue 225 | } 226 | 227 | # Any sub-keys except "Dependents" 228 | if ($BaseKeys.Count -gt 1 -or $BaseKeys.PSChildName -ne 'Dependents') { 229 | continue 230 | } 231 | 232 | $DependentsKey = $BaseKeys[0] 233 | 234 | # Any values under "Dependents" (inc. default value) 235 | $DependentsValues = Get-ItemProperty -Path $DependentsKey.PSPath 236 | if ($DependentsValues) { 237 | continue 238 | } 239 | 240 | # Any sub-keys under "Dependents" 241 | $DependentsKeys = Get-ChildItem -Path $DependentsKey.PSPath 242 | if ($DependentsKeys) { 243 | continue 244 | } 245 | 246 | $Dependency.Status = 'Orphaned' 247 | } 248 | 249 | return ($Results.ToArray() | Sort-Object -Property Name) 250 | } 251 | 252 | Function Resolve-DotNetCliPackagesCacheToRegistry { 253 | [CmdletBinding()] 254 | [OutputType([Void])] 255 | Param( 256 | [Parameter(Mandatory)] 257 | [PSCustomObject[]]$DotNetCliPackages, 258 | 259 | [Parameter(Mandatory)] 260 | [PSCustomObject[]]$RegistryPackages 261 | ) 262 | 263 | foreach ($DncPackage in $DotNetCliPackages) { 264 | $RegistryPackage = $RegistryPackages | Where-Object Name -EQ $DncPackage.Provider 265 | 266 | if (!$RegistryPackage) { 267 | Write-Warning -Message ('Unable to associate {0} to a registry package.' -f $DncPackage.Name) 268 | continue 269 | } 270 | 271 | if ($RegistryPackage.CacheFiles.Count -ne 0) { 272 | Write-Error -Message ('Registry package "{0}" already associated with Dotnet_CLI package but matched: {1}' -f $RegistryPackage.Name, $DncPackage.Name) 273 | continue 274 | } 275 | 276 | $RegistryPackage.CacheFiles.Add($DncPackage.File) 277 | } 278 | } 279 | 280 | Function Resolve-OrphanDependenciesRegistryToCache { 281 | [CmdletBinding()] 282 | [OutputType([Void])] 283 | Param( 284 | [Parameter(Mandatory)] 285 | [PSCustomObject[]]$RegistryPackages 286 | ) 287 | 288 | # Retrieve all cached packages 289 | $Packages = Get-ChildItem -Path $Script:PackageCaches -File -Recurse 290 | 291 | foreach ($RegistryPackage in $RegistryPackages) { 292 | $RegistryPackage.CacheStatus = 'None found' 293 | 294 | # Locate the "known package" entry 295 | $KnownPackage = $null 296 | if ($RegistryPackage.Name -match '^([a-z_.]+)[_.]') { 297 | $KnownPackageName = $Matches[1] 298 | if ($Script:KnownPackages.Keys -contains $KnownPackageName) { 299 | $KnownPackage = $Script:KnownPackages[$KnownPackageName] 300 | } 301 | } 302 | 303 | if (!$KnownPackage) { 304 | $RegistryPackage.CacheStatus = 'Not searched' 305 | continue 306 | } 307 | 308 | # Retrieve the match on the package registry key 309 | if ($RegistryPackage.Name -match $KnownPackage.Registry) { 310 | $PackageRegVersion = $Matches[1] 311 | if ($Matches.Count -ge 3) { 312 | $PackageRegArch = $Matches[2] 313 | } else { 314 | $PackageRegArch = [String]::Empty 315 | } 316 | } else { 317 | $RegistryPackage.CacheStatus = 'Error' 318 | Write-Warning -Message ('[{0}] Known package registry key did not match.' -f $RegistryPackage.Name) 319 | continue 320 | } 321 | 322 | # Filter package cache directories on the regex 323 | $PackageDirs = New-Object -TypeName 'Collections.Generic.List[IO.FileSystemInfo]' 324 | $PackageDirRegex = $KnownPackage.Directory -replace '\$1', $PackageRegVersion -replace '\$2', $PackageRegArch 325 | foreach ($Package in $Packages) { 326 | if ($Package.Directory -match $PackageDirRegex) { 327 | $PackageDirs.Add($Package) 328 | } 329 | } 330 | 331 | if ($PackageDirs.Count -eq 0) { 332 | continue 333 | } 334 | 335 | # Filter package cache files on the regex 336 | $PackageFileRegex = $KnownPackage.File -replace '\$1', $PackageRegVersion -replace '\$2', $PackageRegArch 337 | foreach ($PackageFile in $PackageDirs) { 338 | if ($PackageFile.Name -match $PackageFileRegex) { 339 | $RegistryPackage.CacheFiles.Add($PackageFile) 340 | } 341 | } 342 | 343 | if ($RegistryPackage.CacheFiles.Count -gt 0) { 344 | $RegistryPackage.CacheStatus = '{0} files' -f $RegistryPackage.CacheFiles.Count 345 | 346 | if ($RegistryPackage.CacheFiles.Count -gt 1) { 347 | Write-Warning -Message ('[{0}] Found {1} files in package cache.' -f $RegistryPackage.Name, $RegistryPackage.CacheFiles.Count) 348 | } 349 | } 350 | } 351 | } 352 | 353 | # Determine which file system package caches to inspect 354 | $Script:PackageCaches = @($Script:SysPackageCache) 355 | if (Test-Path -Path $Script:VsPackageCache -PathType Container -ErrorAction Ignore) { 356 | $Script:PackageCaches += $Script:VsPackageCache 357 | } 358 | 359 | $Packages = Find-OrphanDependenciesFromRegistry 360 | if ($Packages) { 361 | Resolve-OrphanDependenciesRegistryToCache -RegistryPackages $Packages 362 | 363 | $DotNetCli = Find-DotNetCliPackagesFromCache 364 | if ($DotNetCli) { 365 | Resolve-DotNetCliPackagesCacheToRegistry -DotNetCliPackages $DotNetCli -RegistryPackages $Packages 366 | } 367 | } 368 | 369 | return $Packages 370 | -------------------------------------------------------------------------------- /Scripts/Get-ControlledGpoStatus.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Check Windows domain GPOs and AGPM server controlled GPOs are in sync 4 | 5 | .DESCRIPTION 6 | Enumerates all GPOs in a Windows domain and controlled GPOs on an AGPM server. Checks are run on the discovered GPOs to determine if the version deployed to the domain matches that on the AGPM server. 7 | 8 | Matching of GPOs between the Windows domain and the AGPM server is performed by GPO GUID. If a domain or AGPM controlled GPO does not have an associated matching GPO this is flagged in the output. 9 | 10 | For GPOs which are matched the following inconsistencies are flagged: 11 | - Name mismatch 12 | - Computer policy version mismatch 13 | - User policy version mismatch 14 | - WMI filter name mismatch 15 | 16 | .PARAMETER AgpmServer 17 | The fully qualified domain name of the AGPM server for which controlled GPOs will be enumerated. 18 | 19 | If a domain was not specified, the default AGPM server will be used as configured in the AGPM Client. 20 | 21 | If a domain was specified, the default AGPM server will be "agpm.". 22 | 23 | .PARAMETER Domain 24 | The fully qualified domain name of the Windows domain for which GPOs will be enumerated. 25 | 26 | The default is the domain of the host system. 27 | 28 | .EXAMPLE 29 | Get-ControlledGpoStatus 30 | 31 | Enumerates all GPOs on the host system's domain against the default AGPM server and verifies they are in sync. 32 | 33 | .NOTES 34 | The following PowerShell modules are required: 35 | - GroupPolicy 36 | Required to enumerate Windows domain GPOs. 37 | Windows Server: Provided by the Group Policy Management feature. 38 | Windows clients: Installed via the Remote Server Administration Tools. 39 | - Microsoft.Agpm 40 | Required to enumerate AGPM server controlled GPOs. 41 | This module is provided by the AGPM Client software. 42 | 43 | Not compatible with PowerShell Core as the dependency modules are themselves not compatible. 44 | 45 | .LINK 46 | https://github.com/ralish/PSWinGlue 47 | #> 48 | 49 | #Requires -Version 3.0 50 | 51 | [CmdletBinding()] 52 | [OutputType([PSCustomObject[]])] 53 | Param( 54 | [ValidateNotNullOrEmpty()] 55 | [String]$Domain, 56 | 57 | [ValidateNotNullOrEmpty()] 58 | [String]$AgpmServer 59 | ) 60 | 61 | $PowerShellCore = New-Object -TypeName Version -ArgumentList 6, 0 62 | if ($PSVersionTable.PSVersion -ge $PowerShellCore -and $PSVersionTable.Platform -ne 'Win32NT') { 63 | throw '{0} is only compatible with Windows.' -f $MyInvocation.MyCommand.Name 64 | } 65 | 66 | # Check required modules are present 67 | $RequiredModules = @('GroupPolicy', 'Microsoft.Agpm') 68 | foreach ($Module in $RequiredModules) { 69 | Write-Verbose -Message ('Checking module is available: {0}' -f $Module) 70 | if (!(Get-Module -Name $Module -ListAvailable -Verbose:$false)) { 71 | throw 'Required module not available: {0}' -f $Module 72 | } 73 | } 74 | 75 | $Results = New-Object -TypeName 'Collections.Generic.List[PSCustomObject]' 76 | 77 | $TypeName = 'PSWinGlue.ControlledGpoStatus' 78 | Update-TypeData -TypeName $TypeName -DefaultDisplayPropertySet @('Name', 'Status') -Force 79 | 80 | # Use the "default" AGPM server for the domain 81 | if ($Domain -and !$AgpmServer) { 82 | $AgpmServer = 'agpm.{0}' -f $Domain 83 | Write-Warning -Message ('Using default AGPM server: {0}' -f $AgpmServer) 84 | } 85 | 86 | # Retrieve domain GPOs 87 | try { 88 | if ($Domain) { 89 | $DomainGPOs = @(Get-GPO -All -Domain $Domain) 90 | } else { 91 | $DomainGPOs = @(Get-GPO -All) 92 | } 93 | } catch { 94 | throw $_ 95 | } 96 | 97 | # Retrieve AGPM GPOs 98 | try { 99 | $DefaultArchiveChanged = $false 100 | 101 | # Overriding the DefaultArchive registry value is unfortunately necessary 102 | # as the Microsoft.Agpm PowerShell module does not respect any configured 103 | # per-domain servers listed under the Domains key, unlike the MMC snap-in. 104 | if ($AgpmServer) { 105 | $AgpmRegPath = 'HKLM:\Software\Microsoft\AGPM' 106 | 107 | if (Test-Path -Path $AgpmRegPath) { 108 | $AgpmReg = Get-Item -Path $AgpmRegPath -ErrorAction Stop 109 | } else { 110 | $AgpmReg = New-Item -Path $AgpmRegPath -ErrorAction Stop 111 | } 112 | 113 | if ($AgpmReg.Property.Contains('DefaultArchive')) { 114 | $OriginalDefaultArchive = $AgpmReg.GetValue('DefaultArchive') 115 | } 116 | 117 | if ($AgpmServer -notmatch ':[1-9][0-9]*$') { 118 | $DefaultArchive = '{0}:4600' -f $AgpmServer 119 | } else { 120 | $DefaultArchive = $AgpmServer 121 | } 122 | 123 | Set-ItemProperty -Path $AgpmRegPath -Name 'DefaultArchive' -Value $DefaultArchive -ErrorAction Stop 124 | $DefaultArchiveChanged = $true 125 | } 126 | 127 | if ($Domain) { 128 | $AgpmGPOs = @(Get-ControlledGpo -Domain $Domain -ErrorAction Stop) 129 | } else { 130 | $AgpmGPOs = @(Get-ControlledGpo -ErrorAction Stop) 131 | } 132 | } catch { 133 | throw $_ 134 | } finally { 135 | if ($DefaultArchiveChanged) { 136 | Set-ItemProperty -Path $AgpmRegPath -Name 'DefaultArchive' -Value $OriginalDefaultArchive 137 | } 138 | } 139 | 140 | # Check the status of all AGPM controlled GPOs 141 | foreach ($AgpmGPO in $AgpmGPOs) { 142 | $Result = [PSCustomObject]@{ 143 | PSTypeName = $TypeName 144 | Name = $AgpmGPO.Name 145 | AGPM = $AgpmGPO 146 | Domain = $null 147 | Status = @() 148 | } 149 | 150 | $DomainGPO = $DomainGPOs | Where-Object Id -EQ $AgpmGPO.ID.TrimStart('{').TrimEnd('}') 151 | if ($DomainGPO) { 152 | $Result.Domain = $DomainGPO 153 | } else { 154 | $Result.Status = @('Only exists in AGPM') 155 | $Results.Add($Result) 156 | continue 157 | } 158 | 159 | # Check display name is in sync 160 | if ($AgpmGPO.Name -ne $DomainGPO.DisplayName) { 161 | $Result.Status += @('Name mismatch') 162 | } 163 | 164 | # Check computer policy is in sync 165 | # 166 | # The casting is necessary as the AGPM version properties are strings. 167 | if ([Int]$AgpmGPO.ComputerVersion -lt $DomainGPO.Computer.DSVersion) { 168 | $Result.Status += @('Domain computer policy is newer (Import)') 169 | } elseif ([Int]$AgpmGPO.ComputerVersion -gt $DomainGPO.Computer.DSVersion) { 170 | $Result.Status += @('AGPM computer policy is newer (Deploy)') 171 | } 172 | 173 | # Check user policy is in sync 174 | # 175 | # The casting is necessary as the AGPM version properties are strings. 176 | if ([Int]$AgpmGPO.UserVersion -lt $DomainGPO.User.DSVersion) { 177 | $Result.Status += @('Domain user policy is newer (Import)') 178 | } elseif ([Int]$AgpmGPO.UserVersion -gt $DomainGPO.User.DSVersion) { 179 | $Result.Status += @('AGPM user policy is newer (Deploy)') 180 | } 181 | 182 | # Check WMI filter is in sync 183 | if ($AgpmGPO.WmiFilterName -or $DomainGPO.WmiFilter) { 184 | if ($AgpmGPO.WmiFilterName -ne $DomainGPO.WmiFilter.Name) { 185 | $Result.Status += @('WMI filter mismatch') 186 | } 187 | } 188 | 189 | if (!$Result.Status) { 190 | $Result.Status = @('OK') 191 | } 192 | 193 | $Results.Add($Result) 194 | } 195 | 196 | # Add any domain GPOs not controlled by AGPM 197 | if ($AgpmGPOs.Count -gt 0) { 198 | $MissingGPOs = $DomainGPOs | Where-Object Id -NotIn $AgpmGPOs.ID.TrimStart('{').TrimEnd('}') 199 | } else { 200 | $MissingGPOs = $DomainGPOs 201 | } 202 | 203 | foreach ($MissingGPO in $MissingGPOs) { 204 | $Result = [PSCustomObject]@{ 205 | PSTypeName = $TypeName 206 | Name = $MissingGPO.DisplayName 207 | AGPM = $null 208 | Domain = $MissingGPO 209 | Status = @('Only exists in Domain') 210 | } 211 | 212 | $Results.Add($Result) 213 | } 214 | 215 | return ($Results.ToArray() | Sort-Object -Property Name) 216 | -------------------------------------------------------------------------------- /Scripts/Get-Fonts.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Retrieves registered fonts 4 | 5 | .DESCRIPTION 6 | Enumerates all registered fonts, performs basic consistency checks, and returns the font name and associated file for each font. 7 | 8 | A font is registered and passes consistency checks if it is listed in the registry and the referenced font file is valid. 9 | 10 | Warnings are printed for registered fonts missing their associated font file and font files missing an associated registration. 11 | 12 | .PARAMETER Scope 13 | Specifies whether to enumerate system fonts or per-user fonts. 14 | 15 | Support for per-user fonts is only available from Windows 10 1809 and Windows Server 2019. 16 | 17 | The default is system fonts. 18 | 19 | .EXAMPLE 20 | Get-Fonts 21 | 22 | Retrieves all registered fonts in the system scope and outputs warnings for any inconsistencies. 23 | 24 | .NOTES 25 | Only OpenType (.otf) and TrueType (.ttf) fonts are supported. 26 | 27 | Per-user fonts are only enumerated in the context of the user executing the function. 28 | 29 | .LINK 30 | https://github.com/ralish/PSWinGlue 31 | #> 32 | 33 | #Requires -Version 3.0 34 | 35 | [CmdletBinding()] 36 | [OutputType([PSCustomObject[]])] 37 | Param( 38 | [ValidateSet('System', 'User')] 39 | [String]$Scope = 'System' 40 | ) 41 | 42 | $PowerShellCore = New-Object -TypeName Version -ArgumentList 6, 0 43 | if ($PSVersionTable.PSVersion -ge $PowerShellCore -and $PSVersionTable.Platform -ne 'Win32NT') { 44 | throw '{0} is only compatible with Windows.' -f $MyInvocation.MyCommand.Name 45 | } 46 | 47 | # Supported font extensions 48 | $ValidExts = @('.otf', '.ttf') 49 | $ValidExtsRegex = '\.(otf|ttf)$' 50 | 51 | Function Get-Fonts { 52 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] 53 | [CmdletBinding()] 54 | [OutputType([PSCustomObject[]])] 55 | Param( 56 | [ValidateSet('System', 'User')] 57 | [String]$Scope = 'System' 58 | ) 59 | 60 | switch ($Scope) { 61 | 'System' { 62 | $FontsFolder = [Environment]::GetFolderPath('Fonts') 63 | $FontsRegKey = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts' 64 | } 65 | 66 | 'User' { 67 | $FontsFolder = Join-Path -Path ([Environment]::GetFolderPath('LocalApplicationData')) -ChildPath 'Microsoft\Windows\Fonts' 68 | $FontsRegKey = 'HKCU:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts' 69 | } 70 | } 71 | 72 | try { 73 | $FontFiles = @(Get-ChildItem -Path $FontsFolder -ErrorAction Stop | Where-Object Extension -In $ValidExts) 74 | } catch [Management.Automation.ItemNotFoundException] { 75 | $FontFiles = @() 76 | } catch { 77 | throw 'Unable to enumerate {0} fonts folder: {1}' -f $Scope.ToLower(), $FontsFolder 78 | } 79 | 80 | try { 81 | $FontsReg = Get-Item -Path $FontsRegKey -ErrorAction Stop 82 | } catch [Management.Automation.ItemNotFoundException] { 83 | $FontsReg = @() 84 | } catch { 85 | throw 'Unable to open {0} fonts registry key: {1}' -f $Scope.ToLower(), $FontsRegKey 86 | } 87 | 88 | $Fonts = New-Object -TypeName 'Collections.Generic.List[PSCustomObject]' 89 | $FontsRegFileNames = New-Object -TypeName 'Collections.Generic.List[String]' 90 | foreach ($FontRegName in ($FontsReg.Property | Sort-Object)) { 91 | $FontRegValue = $FontsReg.GetValue($FontRegName) 92 | 93 | if ($Scope -eq 'User') { 94 | $FontRegFileName = [IO.Path]::GetFileName($FontRegValue) 95 | } else { 96 | $FontRegFileName = $FontRegValue 97 | } 98 | 99 | if ($FontRegFileName -notmatch $ValidExtsRegex) { 100 | Write-Debug -Message ('Ignoring font with unsupported extension: {0} -> {1}' -f $FontRegName, $FontRegFileName) 101 | continue 102 | } elseif ($FontFiles.Name -notcontains $FontRegFileName) { 103 | Write-Warning -Message ('Font file for registered font does not exist: {0} -> {1}' -f $FontRegName, $FontRegFileName) 104 | continue 105 | } 106 | 107 | $Font = [PSCustomObject]@{ 108 | Name = $FontRegName 109 | File = $FontFiles | Where-Object Name -EQ $FontRegFileName 110 | } 111 | 112 | $Fonts.Add($Font) 113 | $FontsRegFileNames.Add($FontRegFileName) 114 | } 115 | 116 | if ($FontFiles.Count -ne 0) { 117 | foreach ($FontFileName in $FontFiles.Name) { 118 | if ($FontFileName -notin $FontsRegFileNames) { 119 | Write-Warning -Message ('Font file not registered for {0}: {1}' -f $Scope.ToLower(), $FontFileName) 120 | } 121 | } 122 | } 123 | 124 | return $Fonts.ToArray() 125 | } 126 | 127 | # Windows 10 1809 and Windows Server 2019 introduced support for installing 128 | # fonts per-user. The corresponding Windows release build number is 17763. 129 | Function Test-PerUserFontsSupported { 130 | [CmdletBinding()] 131 | [OutputType([Boolean])] 132 | Param() 133 | 134 | $BuildNumber = [Int](Get-CimInstance -ClassName 'Win32_OperatingSystem' -Verbose:$false).BuildNumber 135 | if ($BuildNumber -ge 17763) { 136 | return $true 137 | } 138 | 139 | return $false 140 | } 141 | 142 | if ($Scope -eq 'User' -and !(Test-PerUserFontsSupported)) { 143 | throw 'Per-user fonts are only supported from Windows 10 1809 and Windows Server 2019.' 144 | } 145 | 146 | Get-Fonts @PSBoundParameters 147 | -------------------------------------------------------------------------------- /Scripts/Get-InstalledPrograms.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Retrieves installed programs 4 | 5 | .DESCRIPTION 6 | Enumerates all installed programs for the system and current user. 7 | 8 | The results should be nearly identical to those displayed via the "Programs and Features" view of the Windows Control Panel. 9 | 10 | For the "Apps & features" pane of the Settings app the results will be a subset of those displayed (see the Notes section). 11 | 12 | .EXAMPLE 13 | Get-InstalledPrograms 14 | 15 | Retrieves all programs installed system-wide or for the current user. 16 | 17 | .NOTES 18 | Only native Windows applications which register an uninstaller are displayed. 19 | 20 | Microsoft Store apps are not currently enumerated, which the "Apps & features" pane of the Settings app does display. 21 | 22 | The available information displayed for each program is expected to vary, as each program is itself responsible for recording it. 23 | 24 | If the installation date is not explicitly recorded by an installed program, we attempt to derive it based on the last write time of the registry key. 25 | 26 | There is no documented API for enumerating installed native Windows applications, so an approach based off reverse engineering Microsoft's implementation is used. 27 | 28 | There are three registry keys which are inspected to populate the list of installed programs: 29 | - System-wide in native bitness 30 | HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall 31 | - System-wide under the 32-bit emulation layer (64-bit Windows only) 32 | HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall 33 | - Current-user (any bitness) 34 | HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall 35 | 36 | .LINK 37 | https://github.com/ralish/PSWinGlue 38 | #> 39 | 40 | #Requires -Version 3.0 41 | 42 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingEmptyCatchBlock', '')] 43 | [CmdletBinding()] 44 | [OutputType([PSCustomObject[]])] 45 | Param() 46 | 47 | $PowerShellCore = New-Object -TypeName Version -ArgumentList 6, 0 48 | if ($PSVersionTable.PSVersion -ge $PowerShellCore -and $PSVersionTable.Platform -ne 'Win32NT') { 49 | throw '{0} is only compatible with Windows.' -f $MyInvocation.MyCommand.Name 50 | } 51 | 52 | if (!('PSWinGlue.GetInstalledPrograms' -as [Type])) { 53 | $RegQueryInfoKey = @' 54 | [DllImport("advapi32.dll", EntryPoint = "RegQueryInfoKeyW")] 55 | public static extern int RegQueryInfoKey(Microsoft.Win32.SafeHandles.SafeRegistryHandle hKey, 56 | IntPtr lpClass, 57 | IntPtr lpcchClass, 58 | IntPtr lpReserved, 59 | IntPtr lpcSubKeys, 60 | IntPtr lpcbMaxSubKeyLen, 61 | IntPtr lpcbMaxClassLen, 62 | IntPtr lpcValues, 63 | IntPtr lpcbMaxValueNameLen, 64 | IntPtr lpcbMaxValueLen, 65 | IntPtr lpcbSecurityDescriptor, 66 | out UInt64 lpftLastWriteTime); 67 | '@ 68 | 69 | $AddTypeParams = @{} 70 | 71 | if ($PSVersionTable['PSEdition'] -eq 'Core') { 72 | $AddTypeParams['ReferencedAssemblies'] = 'Microsoft.Win32.Registry' 73 | } 74 | 75 | Add-Type -Namespace 'PSWinGlue' -Name 'GetInstalledPrograms' -MemberDefinition $RegQueryInfoKey @AddTypeParams 76 | } 77 | 78 | $TypeName = 'PSWinGlue.InstalledProgram' 79 | Update-TypeData -TypeName $TypeName -DefaultDisplayPropertySet @('Name', 'Publisher', 'Version', 'Scope') -Force 80 | 81 | # System-wide in native bitness 82 | $ComputerNativeRegPath = 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall' 83 | # System-wide under the 32-bit emulation layer (64-bit Windows only) 84 | $ComputerWow64RegPath = 'HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall' 85 | # Current-user (any bitness) 86 | $UserRegPath = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall' 87 | 88 | # Retrieve all installed programs from available keys 89 | $UninstallKeys = Get-ChildItem -Path $ComputerNativeRegPath 90 | if (Test-Path -Path $ComputerWow64RegPath -PathType Container) { 91 | $UninstallKeys += Get-ChildItem -Path $ComputerWow64RegPath 92 | } 93 | if (Test-Path -Path $UserRegPath -PathType Container) { 94 | $UninstallKeys += Get-ChildItem -Path $UserRegPath 95 | } 96 | 97 | # Filter out all the uninteresting installation results 98 | $Results = New-Object -TypeName 'Collections.Generic.List[PSCustomObject]' 99 | foreach ($UninstallKey in $UninstallKeys) { 100 | $Program = Get-ItemProperty -Path $UninstallKey.PSPath 101 | 102 | # Skip any program which doesn't define a display name 103 | if (!$Program.PSObject.Properties['DisplayName']) { 104 | continue 105 | } 106 | 107 | # Skip any program without an uninstall command which is not marked non-removable 108 | if (!($Program.PSObject.Properties['UninstallString'] -or ($Program.PSObject.Properties['NoRemove'] -and $Program.NoRemove -eq 1))) { 109 | continue 110 | } 111 | 112 | # Skip any program which defines a parent program 113 | if ($Program.PSObject.Properties['ParentKeyName'] -or $Program.PSObject.Properties['ParentDisplayName']) { 114 | continue 115 | } 116 | 117 | # Skip any program marked as a system component 118 | if ($Program.PSObject.Properties['SystemComponent'] -and $Program.SystemComponent -eq 1) { 119 | continue 120 | } 121 | 122 | # Skip any program which defines a release type 123 | if ($Program.PSObject.Properties['ReleaseType']) { 124 | continue 125 | } 126 | 127 | $Result = [PSCustomObject]@{ 128 | PSTypeName = $TypeName 129 | PSPath = $Program.PSPath 130 | Name = $Program.DisplayName 131 | Publisher = $null 132 | InstallDate = $null 133 | EstimatedSize = $null 134 | Version = $null 135 | Location = $null 136 | Uninstall = $null 137 | Scope = $null 138 | } 139 | 140 | if ($Program.PSObject.Properties['Publisher']) { 141 | $Result.Publisher = $Program.Publisher 142 | } 143 | 144 | # Try and convert any InstallDate value to a DateTime 145 | if ($Program.PSObject.Properties['InstallDate']) { 146 | $RegInstallDate = $Program.InstallDate 147 | if ($RegInstallDate -match '^[0-9]{8}') { 148 | try { 149 | $Result.InstallDate = New-Object -TypeName 'DateTime' -ArgumentList $RegInstallDate.Substring(0, 4), $RegInstallDate.Substring(4, 2), $RegInstallDate.Substring(6, 2) 150 | } catch { } 151 | } 152 | 153 | if (!$Result.InstallDate) { 154 | Write-Warning -Message ('[{0}] Registry key has invalid value for InstallDate: {1}' -f $Program.DisplayName, $RegInstallDate) 155 | } 156 | } 157 | 158 | # Fall back to the last write time of the registry key 159 | if (!$Result.InstallDate) { 160 | [UInt64]$RegLastWriteTime = 0 161 | $Status = [PSWinGlue.GetInstalledPrograms]::RegQueryInfoKey($UninstallKey.Handle, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, [ref]$RegLastWriteTime) 162 | 163 | if ($Status -eq 0) { 164 | $Result.InstallDate = [DateTime]::FromFileTime($RegLastWriteTime) 165 | } else { 166 | Write-Warning -Message ('[{0}] Retrieving registry key last write time failed with status: {1}' -f $Program.DisplayName, $Status) 167 | } 168 | } 169 | 170 | if ($Program.PSObject.Properties['EstimatedSize']) { 171 | $Result.EstimatedSize = $Program.EstimatedSize 172 | } 173 | 174 | if ($Program.PSObject.Properties['DisplayVersion']) { 175 | $Result.Version = $Program.DisplayVersion 176 | } 177 | 178 | if ($Program.PSObject.Properties['InstallLocation']) { 179 | $Result.Location = $Program.InstallLocation 180 | } 181 | 182 | if ($Program.PSObject.Properties['UninstallString']) { 183 | $Result.Uninstall = $Program.UninstallString 184 | } 185 | 186 | if ($Program.PSPath.startswith('Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE')) { 187 | $Result.Scope = 'System' 188 | } else { 189 | $Result.Scope = 'User' 190 | } 191 | 192 | $Results.Add($Result) 193 | } 194 | 195 | return ($Results.ToArray() | Sort-Object -Property Name) 196 | -------------------------------------------------------------------------------- /Scripts/Get-TaskSchedulerEvent.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Retrieves events matching the specified IDs from the Task Scheduler event log 4 | 5 | .DESCRIPTION 6 | Retrieves specified event IDs from the Task Scheduler event log, up to a maximum number of events, with optional filtering of specific task names. 7 | 8 | .PARAMETER EventIds 9 | The event IDs to query for. 10 | 11 | The default is to query for event IDs which represent a scheduled task failure: 111, 202, 203, 323, 329, 331. 12 | 13 | .PARAMETER IgnoredTasks 14 | An optional array of task names to filter out of the returned results. 15 | 16 | The default is to exclude several tasks for which failure is typically benign: 17 | - \Microsoft\Windows\.NET Framework\.NET Framework NGEN v4.0.30319 18 | - \Microsoft\Windows\.NET Framework\.NET Framework NGEN v4.0.30319 64 19 | - \Microsoft\Windows\NetCfg\BindingWorkItemQueueHandler 20 | - \Microsoft\Windows\Shell\CreateObjectTas 21 | 22 | Note that filtering of ignored tasks is performed after the specified maximum number of events have been returned. 23 | 24 | .PARAMETER MaxEvents 25 | Maximum number of events to return. 26 | 27 | The default is 100 events. 28 | 29 | Note that this parameter interacts with the IgnoredTasks parameter in a way that may be counter-intuitive. 30 | 31 | .EXAMPLE 32 | Get-TaskSchedulerEvent 33 | 34 | Retrieves the most recent 100 events indicating a scheduled task failure, ignoring typically benign task failures. 35 | 36 | .NOTES 37 | Task Scheduler events 38 | https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2008-R2-and-2008/dd315533(v%3dws.10)#events 39 | 40 | .LINK 41 | https://github.com/ralish/PSWinGlue 42 | #> 43 | 44 | #Requires -Version 3.0 45 | 46 | [CmdletBinding()] 47 | [OutputType([Diagnostics.Eventing.Reader.EventRecord[]])] 48 | Param( 49 | [ValidateNotNullOrEmpty()] 50 | [Int[]]$EventIds = @(111, 202, 203, 323, 329, 331), 51 | 52 | [ValidateRange(1, 1000)] 53 | [Int]$MaxEvents = 100, 54 | 55 | [String[]]$IgnoredTasks = @( 56 | '\Microsoft\Windows\.NET Framework\.NET Framework NGEN v4.0.30319', 57 | '\Microsoft\Windows\.NET Framework\.NET Framework NGEN v4.0.30319 64', 58 | '\Microsoft\Windows\NetCfg\BindingWorkItemQueueHandler', 59 | '\Microsoft\Windows\Shell\CreateObjectTask' 60 | ) 61 | ) 62 | 63 | $PowerShellCore = New-Object -TypeName Version -ArgumentList 6, 0 64 | if ($PSVersionTable.PSVersion -ge $PowerShellCore -and $PSVersionTable.Platform -ne 'Win32NT') { 65 | throw '{0} is only compatible with Windows.' -f $MyInvocation.MyCommand.Name 66 | } 67 | 68 | $WinEventParams = @{ 69 | FilterHashtable = @{ 70 | ProviderName = 'Microsoft-Windows-TaskScheduler' 71 | ID = $EventIds 72 | } 73 | 74 | MaxEvents = $MaxEvents 75 | ErrorAction = 'Stop' 76 | } 77 | 78 | $Events = [Collections.Generic.List[Diagnostics.Eventing.Reader.EventRecord]]@(Get-WinEvent @WinEventParams) 79 | 80 | if ($IgnoredTasks) { 81 | $FilteredEvents = New-Object -TypeName 'Collections.Generic.List[Diagnostics.Eventing.Reader.EventRecord]' 82 | 83 | foreach ($Event in $Events) { 84 | $EventXml = [Xml]$Event.ToXml() 85 | $EventData = $EventXml.Event.EventData.Data 86 | $TaskName = $EventData | Where-Object Name -EQ 'TaskName' 87 | 88 | if ($TaskName -notin $IgnoredTasks) { 89 | $FilteredEvents.Add($Event) 90 | } 91 | } 92 | 93 | $Events = $FilteredEvents 94 | } 95 | 96 | return $Events.ToArray() 97 | -------------------------------------------------------------------------------- /Scripts/Hide-SilverlightUpdates.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Hides Silverlight updates 4 | 5 | .DESCRIPTION 6 | On some releases of Windows which have opted-in to Microsoft Update, the Silverlight application framework may be offered. 7 | 8 | While these updates can be hidden via the Windows Update user interface, doing so will result in the previous Silverlight update (i.e. the one which was superseded by the now hidden update) being offered. 9 | 10 | Hiding all Silverlight updates will typically take many "scan and hide" iterations. This function will scan for and hide all Silverlight updates in a single invocation. 11 | 12 | .EXAMPLE 13 | Hide-SilverlightUpdates 14 | 15 | Enumerates available updates and hides those with Silverlight in the title. 16 | 17 | .NOTES 18 | Administrator privileges are required to modify the visibility of updates. 19 | 20 | This is a PowerShell implementation of a WSH solution: 21 | https://superuser.com/a/1009947 22 | 23 | .LINK 24 | https://github.com/ralish/PSWinGlue 25 | #> 26 | 27 | #Requires -Version 3.0 28 | 29 | [CmdletBinding()] 30 | [OutputType([Void])] 31 | Param() 32 | 33 | $PowerShellCore = New-Object -TypeName Version -ArgumentList 6, 0 34 | if ($PSVersionTable.PSVersion -ge $PowerShellCore -and $PSVersionTable.Platform -ne 'Win32NT') { 35 | throw '{0} is only compatible with Windows.' -f $MyInvocation.MyCommand.Name 36 | } 37 | 38 | $User = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent() 39 | if (!$User.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { 40 | throw '{0} requires Administrator privileges.' -f $MyInvocation.MyCommand.Name 41 | } 42 | 43 | $UpdateSession = $null 44 | $UpdateSearcher = $null 45 | 46 | try { 47 | $UpdateSession = New-Object -ComObject 'Microsoft.Update.Session' 48 | 49 | $UpdateSearcher = $UpdateSession.CreateUpdateSearcher() 50 | $UpdateSearcher.Online = $false 51 | 52 | do { 53 | $UpdatesFound = $false 54 | 55 | $SearchResults = $UpdateSearcher.Search('IsHidden=0 And IsInstalled=0') 56 | $SearchUpdates = $SearchResults.Updates 57 | 58 | for ($i = 0; $i -lt $SearchUpdates.Count; $i++) { 59 | $SearchUpdate = $SearchUpdates.Item($i) 60 | 61 | if ($SearchUpdate.Title -match 'Silverlight') { 62 | $UpdatesFound = $true 63 | 64 | Write-Verbose -Message ('Hiding update: {0}' -f $Update.Title) 65 | $Update.IsHidden = $true 66 | } 67 | 68 | $null = [Runtime.InteropServices.Marshal]::ReleaseComObject($SearchUpdate) 69 | } 70 | 71 | $null = [Runtime.InteropServices.Marshal]::ReleaseComObject($SearchUpdates) 72 | $null = [Runtime.InteropServices.Marshal]::ReleaseComObject($SearchResults) 73 | } while ($UpdatesFound) 74 | } catch { 75 | throw $_ 76 | } finally { 77 | if ($UpdateSearcher) { $null = [Runtime.InteropServices.Marshal]::ReleaseComObject($UpdateSearcher) } 78 | if ($UpdateSession) { $null = [Runtime.InteropServices.Marshal]::ReleaseComObject($UpdateSession) } 79 | } 80 | -------------------------------------------------------------------------------- /Scripts/Install-ExcelAddin.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Installs Excel add-ins 4 | 5 | .DESCRIPTION 6 | This function is designed to assist with handling two common installation scenarios for Excel add-ins: 7 | 8 | - A program which provides an Excel add-in is installed for all users but doesn't handle installation of the add-in for all users 9 | - A custom Excel add-in needs to be installed for all users (e.g. from a network share) without its own separate installer system 10 | 11 | In both scenarios the challenge is in silently automating the installation due to the interactive nature of the Excel object model. 12 | 13 | .PARAMETER Path 14 | The path to an individual Excel add-in to install, or a directory containing Excel add-ins to install. 15 | 16 | .PARAMETER Copy 17 | Copy each add-in to the Office add-ins path on the computer for the running user and perform installation from this path. 18 | 19 | This switch is primarily designed for use when add-in(s) reside on network storage and so may not always be accessible. 20 | 21 | If ommitted, add-ins will be installed directly from the provided path under the assumption it will remain accessible. 22 | 23 | .PARAMETER Reinstall 24 | Reinstall add-ins for which an existing add-in with the same file name and extension is already installed. 25 | 26 | This switch is especially useful with the Copy switch to ensure the latest version of add-in(s) are installed. 27 | 28 | If ommitted, add-ins with a file name and extension which match an existing installed add-in will be skipped. 29 | 30 | .EXAMPLE 31 | Install-ExcelAddin -Path 'C:\Program Files\Acme Inc\Excel Addins' -Reinstall 32 | 33 | Install Excel add-ins found in the "C:\Program Files\Acme Inc\Excel Addins" directory. 34 | 35 | .NOTES 36 | Excel add-ins must have one of the following file extensions: 37 | - XLA Excel 97-2003 Add-in 38 | - XLAM Excel Add-in 39 | 40 | .LINK 41 | https://github.com/ralish/PSWinGlue 42 | #> 43 | 44 | #Requires -Version 3.0 45 | 46 | [CmdletBinding()] 47 | [OutputType([Void])] 48 | Param( 49 | [Parameter(Mandatory)] 50 | [String]$Path, 51 | 52 | [Switch]$Copy, 53 | [Switch]$Reinstall 54 | ) 55 | 56 | $PowerShellCore = New-Object -TypeName Version -ArgumentList 6, 0 57 | if ($PSVersionTable.PSVersion -ge $PowerShellCore -and $PSVersionTable.Platform -ne 'Win32NT') { 58 | throw '{0} is only compatible with Windows.' -f $MyInvocation.MyCommand.Name 59 | } 60 | 61 | # Valid add-in extensions 62 | $ValidExts = @('.xla', '.xlam') 63 | 64 | $AddinsPath = Get-Item -Path $Path -ErrorAction Stop 65 | if ($AddinsPath -is [IO.FileInfo]) { 66 | if ($AddinsPath.Extension -notin $ValidExts) { 67 | throw 'File is not an Excel add-in (xla/xlam).' 68 | } 69 | 70 | $Addins = @($AddinsPath) 71 | } elseif ($AddinsPath -is [IO.DirectoryInfo]) { 72 | $Addins = @(Get-ChildItem -Path $AddinsPath | Where-Object Extension -In $ValidExts) 73 | 74 | if ($Addins.Count -eq 0) { 75 | Write-Warning -Message 'Directory has no Excel add-ins (xla/xlam).' 76 | return 77 | } 78 | } else { 79 | throw 'Path must be an add-in file or a directory containing add-ins.' 80 | } 81 | 82 | $OfficeAddinsPath = Join-Path -Path $env:APPDATA -ChildPath 'Microsoft\AddIns' 83 | Write-Debug -Message ('Office add-ins path: {0}' -f $OfficeAddinsPath) 84 | 85 | try { 86 | Write-Debug -Message 'Creating Excel COM object ...' 87 | $Excel = New-Object -ComObject 'Excel.Application' 88 | } catch { 89 | throw $_ 90 | } 91 | 92 | try { 93 | $ExcelAddins = $null 94 | $ExcelWorkbooks = $null 95 | $ExcelWorkbook = $null 96 | 97 | # Stores enumerated Excel add-ins for improved performance 98 | $ExcelAddinsList = New-Object -TypeName 'Collections.Generic.List[Object]' 99 | 100 | Write-Debug -Message 'Retrieving Excel add-ins ...' 101 | $ExcelAddins = $Excel.AddIns 102 | # The add-ins list exposed by the Excel object model is indexed from one! 103 | for ($i = 1; $i -le $ExcelAddins.Count; $i++) { 104 | $null = $ExcelAddinsList.Add($ExcelAddins.Item($i)) 105 | } 106 | 107 | foreach ($Addin in $Addins) { 108 | if ($ExcelAddinsList.Name -contains $Addin.Name) { 109 | Write-Verbose -Message ('Excel add-in already installed: {0}' -f $Addin.Name) 110 | if (!$Reinstall) { 111 | continue 112 | } 113 | } 114 | 115 | if ($Copy) { 116 | if (!(Test-Path -Path $OfficeAddinsPath -PathType Container)) { 117 | try { 118 | $null = New-Item -Path $OfficeAddinsPath -ItemType Directory -Force -ErrorAction Stop 119 | } catch { 120 | throw ('Unable to create Office add-ins directory: {0}' -f $OfficeAddinsPath) 121 | } 122 | } 123 | 124 | try { 125 | Copy-Item -Path $Addin.FullName -Destination $OfficeAddinsPath -Force -ErrorAction Stop 126 | } catch { 127 | throw ('Unable to copy add-in to Office add-ins directory: {0}' -f $Addin.Name) 128 | } 129 | } 130 | 131 | # Excel.AddIns.Add() requires an open workbook. We'll only open one if 132 | # we find an add-in to install, which we'll re-use for any subsequent 133 | # add-ins to install in this run. 134 | if (!$ExcelWorkbook) { 135 | if (!$ExcelWorkbooks) { 136 | Write-Debug -Message 'Retrieving open workbooks ...' 137 | $ExcelWorkbooks = $Excel.Workbooks 138 | } 139 | 140 | Write-Debug -Message 'Creating new workbook ...' 141 | $ExcelWorkbook = $ExcelWorkbooks.Add() 142 | } 143 | 144 | Write-Debug -Message ('Adding Excel add-in: {0}' -f $Addin.Name) 145 | $ExcelAddin = $ExcelAddins.Add($Addin.FullName, $false) 146 | $ExcelAddin.Installed = $true 147 | $null = [Runtime.InteropServices.Marshal]::ReleaseComObject($ExcelAddin) 148 | 149 | Write-Verbose -Message ('Installed Excel add-in: {0}' -f $Addin.Name) 150 | } 151 | } finally { 152 | if ($ExcelAddinsList.Count -gt 0) { 153 | foreach ($ExcelAddin in $ExcelAddinsList) { 154 | $null = [Runtime.InteropServices.Marshal]::ReleaseComObject($ExcelAddin) 155 | } 156 | } 157 | $null = [Runtime.InteropServices.Marshal]::ReleaseComObject($ExcelAddins) 158 | 159 | if ($ExcelWorkbook) { 160 | Write-Debug -Message 'Closing Excel workbook ...' 161 | $ExcelWorkbook.Close($false) 162 | $null = [Runtime.InteropServices.Marshal]::ReleaseComObject($ExcelWorkbook) 163 | } 164 | 165 | if ($ExcelWorkbooks) { 166 | $null = [Runtime.InteropServices.Marshal]::ReleaseComObject($ExcelWorkbooks) 167 | } 168 | 169 | Write-Debug -Message 'Quitting Excel ...' 170 | $Excel.Quit() 171 | $null = [Runtime.InteropServices.Marshal]::ReleaseComObject($Excel) 172 | } 173 | -------------------------------------------------------------------------------- /Scripts/Install-Font.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Installs a specific font or all fonts from a directory 4 | 5 | .DESCRIPTION 6 | Provides support for installation of fonts using either the Windows shell or an emulated approach without shell dependencies. 7 | 8 | The two supported methods of font installation each have their own benefits and drawbacks. See the parameter help for more details. 9 | 10 | .PARAMETER Path 11 | The path to an individual font to install, or a directory containing fonts to install. 12 | 13 | .PARAMETER Scope 14 | Specifies whether to install fonts system-wide or per-user. 15 | 16 | Support for per-user fonts is only available from Windows 10 1809 and Windows Server 2019. 17 | 18 | Installing system-wide fonts requires Administrator privileges. 19 | 20 | The default is system-wide. 21 | 22 | .PARAMETER Method 23 | Specifies the method to use for installation of fonts. 24 | 25 | Two methods are currently supported: 26 | - Manual: An approach which emulates the behaviour of the Windows shell (default) 27 | - Shell: Directly use the Windows shell facilities for installing fonts 28 | 29 | The Manual approach is the default and should be safe to use in unattended scenarios. Although untested, it should be compatible with Server Core installations. 30 | 31 | The Shell approach is not safe to use in unattended scenarios as in some instances it may present interactive prompts (e.g. overwriting an existing font). 32 | 33 | .PARAMETER UninstallExisting 34 | If a font of the same name is already installed, uninstall it first before installing the provided font. 35 | 36 | This parameter only applies to the Manual install method, as the Shell install method provides its own support. 37 | 38 | Uninstallation will only be attempted if the computed hash of the existing font is different from the provided font. 39 | 40 | When calling Install-Font.ps1 directly (not via the PSWinGlue module), ensure Uninstall-Font.ps1 is in the same directory. 41 | 42 | .EXAMPLE 43 | Install-Fonts -Path C:\Fonts 44 | 45 | Installs all fonts from the "C:\Fonts" directory. Fonts will be installed system-wide using the "Manual" method. 46 | 47 | .EXAMPLE 48 | Install-Fonts -Path "$HOME\Fonts" -Scope User -Method Shell 49 | 50 | Installs all fonts from the "Fonts" folder in the user's home directory. Fonts will be installed only for the running user using the "Shell" method. 51 | 52 | .NOTES 53 | Only OpenType (.otf) and TrueType (.ttf) fonts are supported. 54 | 55 | Per-user fonts are only installed in the context of the user executing the function. 56 | 57 | .LINK 58 | https://github.com/ralish/PSWinGlue 59 | #> 60 | 61 | # Get-FileHash shipped with PowerShell 4.0 62 | #Requires -Version 4.0 63 | 64 | [CmdletBinding(SupportsShouldProcess)] 65 | [OutputType([Void])] 66 | Param( 67 | [ValidateNotNullOrEmpty()] 68 | [String]$Path, 69 | 70 | [ValidateSet('System', 'User')] 71 | [String]$Scope = 'System', 72 | 73 | [ValidateSet('Manual', 'Shell')] 74 | [String]$Method = 'Manual', 75 | 76 | [Switch]$UninstallExisting 77 | ) 78 | 79 | $PowerShellMin = New-Object -TypeName Version -ArgumentList 4, 0 80 | if ($PSVersionTable.PSVersion -lt $PowerShellMin) { 81 | throw '{0} requires at least PowerShell {1}.' -f $MyInvocation.MyCommand.Name, $PowerShellMin 82 | } 83 | 84 | $PowerShellCore = New-Object -TypeName Version -ArgumentList 6, 0 85 | if ($PSVersionTable.PSVersion -ge $PowerShellCore -and $PSVersionTable.Platform -ne 'Win32NT') { 86 | throw '{0} is only compatible with Windows.' -f $MyInvocation.MyCommand.Name 87 | } 88 | 89 | # Supported font extensions 90 | $ValidExts = @('.otf', '.ttf') 91 | $ValidExtsRegex = '\.(otf|ttf)$' 92 | 93 | Function Get-Fonts { 94 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseOutputTypeCorrectly', '')] # PSScriptAnalyzer bug? 95 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] 96 | [CmdletBinding()] 97 | [OutputType([Collections.Generic.List[PSCustomObject]])] 98 | Param( 99 | [ValidateSet('System', 'User')] 100 | [String]$Scope = 'System' 101 | ) 102 | 103 | switch ($Scope) { 104 | 'System' { 105 | $FontsFolder = [Environment]::GetFolderPath('Fonts') 106 | $FontsRegKey = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts' 107 | } 108 | 109 | 'User' { 110 | $FontsFolder = Join-Path -Path ([Environment]::GetFolderPath('LocalApplicationData')) -ChildPath 'Microsoft\Windows\Fonts' 111 | $FontsRegKey = 'HKCU:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts' 112 | } 113 | } 114 | 115 | try { 116 | $FontFiles = @(Get-ChildItem -Path $FontsFolder -ErrorAction Stop | Where-Object Extension -In $ValidExts) 117 | } catch [Management.Automation.ItemNotFoundException] { 118 | $FontFiles = @() 119 | } catch { 120 | throw 'Unable to enumerate {0} fonts folder: {1}' -f $Scope.ToLower(), $FontsFolder 121 | } 122 | 123 | try { 124 | $FontsReg = Get-Item -Path $FontsRegKey -ErrorAction Stop 125 | } catch [Management.Automation.ItemNotFoundException] { 126 | $FontsReg = @() 127 | } catch { 128 | throw 'Unable to open {0} fonts registry key: {1}' -f $Scope.ToLower(), $FontsRegKey 129 | } 130 | 131 | $Fonts = New-Object -TypeName 'Collections.Generic.List[PSCustomObject]' 132 | $FontsRegFileNames = New-Object -TypeName 'Collections.Generic.List[String]' 133 | foreach ($FontRegName in ($FontsReg.Property | Sort-Object)) { 134 | $FontRegValue = $FontsReg.GetValue($FontRegName) 135 | 136 | if ($Scope -eq 'User') { 137 | $FontRegFileName = [IO.Path]::GetFileName($FontRegValue) 138 | } else { 139 | $FontRegFileName = $FontRegValue 140 | } 141 | 142 | if ($FontRegFileName -notmatch $ValidExtsRegex) { 143 | Write-Debug -Message ('Ignoring font with unsupported extension: {0} -> {1}' -f $FontRegName, $FontRegFileName) 144 | continue 145 | } elseif ($FontFiles.Name -notcontains $FontRegFileName) { 146 | Write-Warning -Message ('Font file for registered font does not exist: {0} -> {1}' -f $FontRegName, $FontRegFileName) 147 | continue 148 | } 149 | 150 | $Font = [PSCustomObject]@{ 151 | Name = $FontRegName 152 | File = $FontFiles | Where-Object Name -EQ $FontRegFileName 153 | } 154 | 155 | $Fonts.Add($Font) 156 | $FontsRegFileNames.Add($FontRegFileName) 157 | } 158 | 159 | return , $Fonts 160 | } 161 | 162 | Function Install-FontManual { 163 | [CmdletBinding(SupportsShouldProcess)] 164 | [OutputType([Void])] 165 | Param( 166 | [Parameter(Mandatory)] 167 | [Collections.Generic.List[IO.FileInfo]]$Fonts, 168 | 169 | [ValidateSet('System', 'User')] 170 | [String]$Scope = 'System', 171 | 172 | [Switch]$UninstallExisting 173 | ) 174 | 175 | Begin { 176 | switch ($Scope) { 177 | 'System' { 178 | $FontsFolder = [Environment]::GetFolderPath('Fonts') 179 | $FontsRegKey = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts' 180 | } 181 | 182 | 'User' { 183 | $FontsFolder = Join-Path -Path ([Environment]::GetFolderPath('LocalApplicationData')) -ChildPath 'Microsoft\Windows\Fonts' 184 | $FontsRegKey = 'HKCU:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts' 185 | } 186 | } 187 | 188 | if ($Scope -eq 'User') { 189 | $null = New-Item -Path $FontsFolder -ItemType Directory -ErrorAction Ignore 190 | $null = New-Item -Path $FontsRegKey -ErrorAction Ignore 191 | } 192 | 193 | try { 194 | $FontsReg = Get-Item -Path $FontsRegKey -ErrorAction Stop 195 | } catch { 196 | throw 'Unable to open {0} fonts registry key: {1}' -f $Scope.ToLower(), $FontsRegKey 197 | } 198 | 199 | Add-Type -AssemblyName 'PresentationCore' -ErrorAction Stop 200 | } 201 | 202 | Process { 203 | foreach ($Font in $Fonts) { 204 | $FontUri = New-Object -TypeName 'Uri' -ArgumentList $Font.FullName 205 | 206 | try { 207 | $FontPackage = New-FontMemoryPackage -Font $FontUri 208 | } catch { 209 | Write-Error -Message ('Unable to read font: {0}' -f $Font.Name) 210 | continue 211 | } 212 | 213 | try { 214 | $GlyphTypeface = New-Object -TypeName 'Windows.Media.GlyphTypeface' -ArgumentList $FontPackage.PackUri 215 | } catch { 216 | Write-Error -Message ('Unable to import font: {0}' -f $Font.Name) 217 | Remove-FontMemoryPackage -FontPackage $FontPackage 218 | continue 219 | } 220 | 221 | $FontCulture = 'en-US' 222 | if ($GlyphTypeface.Win32FamilyNames.ContainsKey($FontCulture) -and $GlyphTypeface.Win32FaceNames.ContainsKey($FontCulture)) { 223 | $FontFamilyName = $GlyphTypeface.Win32FamilyNames[$FontCulture] 224 | $FontFaceName = $GlyphTypeface.Win32FaceNames[$FontCulture] 225 | } else { 226 | Write-Error -Message ('Font does not contain metadata for {0} culture: {1}' -f $FontCulture, $Font.Name) 227 | Remove-FontMemoryPackage -FontPackage $FontPackage 228 | continue 229 | } 230 | 231 | # Matches the convention used by the Explorer shell 232 | $FontInstallPath = Join-Path -Path $FontsFolder -ChildPath $Font.Name 233 | $FontInstallSuffixNum = -1 234 | while (Test-Path -Path $FontInstallPath) { 235 | $FontInstallSuffixNum++ 236 | $FontInstallName = '{0}_{1}{2}' -f $Font.BaseName, $FontInstallSuffixNum, $Font.Extension 237 | $FontInstallPath = Join-Path -Path $FontsFolder -ChildPath $FontInstallName 238 | } 239 | Write-Debug -Message ('[{0}] Font install path: {1}' -f $Font.Name, $FontInstallPath) 240 | 241 | # Matches the convention used by the Explorer shell 242 | if ($FontFaceName -eq 'Regular') { 243 | $FontRegName = '{0} (TrueType)' -f $FontFamilyName 244 | } else { 245 | $FontRegName = '{0} {1} (TrueType)' -f $FontFamilyName, $FontFaceName 246 | } 247 | Write-Debug -Message ('[{0}] Font registry name: {1}' -f $Font.Name, $FontRegName) 248 | 249 | if ($Scope -eq 'User') { 250 | $FontRegValue = $FontInstallPath 251 | } else { 252 | $FontRegValue = [IO.Path]::GetFileName($FontInstallPath) 253 | } 254 | 255 | if ($FontsReg.Property.Contains($FontRegName)) { 256 | if (!$UninstallExisting) { 257 | Write-Error -Message ('Font registry name already exists: {0}' -f $FontRegName) 258 | Remove-FontMemoryPackage -FontPackage $FontPackage 259 | continue 260 | } 261 | 262 | $ExistingFontRegValue = $FontsReg.GetValue($FontRegName) 263 | if ($Scope -eq 'User') { 264 | $ExistingFontRegFileName = [IO.Path]::GetFileName($ExistingFontRegValue) 265 | } else { 266 | $ExistingFontRegFileName = $ExistingFontRegValue 267 | } 268 | 269 | Write-Verbose -Message ('Uninstalling existing font: {0}' -f $ExistingFontRegFileName) 270 | & $UninstallFont -Name $FontRegName -Scope $Scope 271 | } 272 | 273 | if ($PSCmdlet.ShouldProcess($Font.Name, 'Install font manually')) { 274 | Write-Verbose -Message ('Installing font manually: {0}' -f $Font.Name) 275 | Copy-Item -Path $Font.FullName -Destination $FontInstallPath 276 | $null = New-ItemProperty -Path $FontsRegKey -Name $FontRegName -PropertyType String -Value $FontRegValue 277 | } 278 | 279 | Remove-FontMemoryPackage -FontPackage $FontPackage 280 | } 281 | } 282 | } 283 | 284 | Function Install-FontShell { 285 | [CmdletBinding(SupportsShouldProcess)] 286 | [OutputType([Void])] 287 | Param( 288 | [Parameter(Mandatory)] 289 | [Collections.Generic.List[IO.FileInfo]]$Fonts 290 | ) 291 | 292 | Begin { 293 | # ShellSpecialFolderConstants enumeration 294 | # https://learn.microsoft.com/en-us/windows/desktop/api/Shldisp/ne-shldisp-shellspecialfolderconstants 295 | $ssfFONTS = 20 296 | 297 | # _SHFILEOPSTRUCTA structure 298 | # https://learn.microsoft.com/en-us/windows/desktop/api/shellapi/ns-shellapi-_shfileopstructa 299 | $FOF_SILENT = 4 300 | $FOF_NOCONFIRMATION = 16 301 | $FOF_NOERRORUI = 1024 302 | $FOF_NOCOPYSECURITYATTRIBS = 2048 303 | $CopyOptions = $FOF_SILENT + $FOF_NOCONFIRMATION + $FOF_NOERRORUI + $FOF_NOCOPYSECURITYATTRIBS 304 | 305 | $ShellApp = $null 306 | $FontsFolder = $null 307 | 308 | try { 309 | $ShellApp = New-Object -ComObject 'Shell.Application' 310 | $FontsFolder = $ShellApp.NameSpace($ssfFONTS) 311 | } catch { 312 | if ($ShellApp) { $null = [Runtime.InteropServices.Marshal]::ReleaseComObject($ShellApp) } 313 | throw $_ 314 | } 315 | } 316 | 317 | Process { 318 | foreach ($Font in $Fonts) { 319 | if ($PSCmdlet.ShouldProcess($Font.Name, 'Install font via shell')) { 320 | Write-Verbose -Message ('Installing font via shell: {0}' -f $Font.Name) 321 | $FontsFolder.CopyHere($Font.FullName, $CopyOptions) 322 | } 323 | } 324 | } 325 | 326 | End { 327 | $null = [Runtime.InteropServices.Marshal]::ReleaseComObject($FontsFolder) 328 | $null = [Runtime.InteropServices.Marshal]::ReleaseComObject($ShellApp) 329 | } 330 | } 331 | 332 | # Huge credit to Stipo for this StackOverflow answer: 333 | # https://stackoverflow.com/a/31278196 334 | Function New-FontMemoryPackage { 335 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '')] 336 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] 337 | [CmdletBinding()] 338 | [OutputType([PSCustomObject])] 339 | Param( 340 | [Parameter(Mandatory)] 341 | [Uri]$Font 342 | ) 343 | 344 | $PowerShellCore = New-Object -TypeName Version -ArgumentList 6, 0 345 | if ($PSVersionTable.PSVersion -ge $PowerShellCore) { 346 | Add-Type -AssemblyName 'System.IO.Packaging' -ErrorAction Stop 347 | } else { 348 | Add-Type -AssemblyName 'WindowsBase' -ErrorAction Stop 349 | } 350 | 351 | # WPF may perform caching on the package URI so we keep it unique 352 | if (Get-Variable -Name 'PSWinGluePackageCounter' -Scope Global -ErrorAction Ignore) { 353 | $global:PSWinGluePackageCounter++ 354 | } else { 355 | $global:PSWinGluePackageCounter = 1 356 | } 357 | 358 | # Create a memory-backed package for the font 359 | $PackageUriRaw = 'payload://memorypackage{0}' -f $global:PSWinGluePackageCounter 360 | $PackageUri = New-Object -TypeName 'Uri' -ArgumentList ($PackageUriRaw, [UriKind]::Absolute) 361 | $PackageStream = New-Object -TypeName 'IO.MemoryStream' 362 | $Package = [IO.Packaging.Package]::Open($PackageStream, [IO.FileMode]::Create) 363 | [IO.Packaging.PackageStore]::AddPackage($PackageUri, $Package) 364 | 365 | # Create the package part for the font 366 | $PartUri = New-Object -TypeName 'Uri' -ArgumentList ('/stream1', [UriKind]::Relative) 367 | $Part = $Package.CreatePart($PartUri, 'application/octet-stream') 368 | 369 | # Package URIs must be globally unique due to WPF caching 370 | $PackUri = [IO.Packaging.PackUriHelper]::Create($PackageUri, $PartUri) 371 | 372 | $Result = [PSCustomObject]@{ 373 | Package = $Package 374 | PackageUri = $PackageUri 375 | PackUri = $PackUri 376 | } 377 | Write-Debug -Message ('Created font package: {0}' -f $Result.PackUri) 378 | 379 | # Read the entire font into memory 380 | try { 381 | $FontBytes = [IO.File]::ReadAllBytes($Font.LocalPath) 382 | } catch { 383 | Remove-FontMemoryPackage -FontPackage $F 384 | throw $_ 385 | } 386 | $FontStream = New-Object -TypeName 'IO.MemoryStream' -ArgumentList (, $FontBytes) 387 | 388 | # Copy the font bytes into the part 389 | $PartBufferSize = 4096 390 | $PartBuffer = New-Object -TypeName 'Byte[]' -ArgumentList $PartBufferSize 391 | $PartStream = $Part.GetStream() 392 | while (($BytesRead = $FontStream.Read($PartBuffer, 0, $PartBufferSize)) -ne 0) { 393 | $PartStream.Write($PartBuffer, 0, $BytesRead) 394 | } 395 | $PartStream.Dispose() 396 | 397 | return $Result 398 | } 399 | 400 | Function Remove-FontMemoryPackage { 401 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] 402 | [CmdletBinding()] 403 | [OutputType([Void])] 404 | Param( 405 | [Parameter(Mandatory)] 406 | [PSCustomObject]$FontPackage 407 | ) 408 | 409 | Write-Debug -Message ('Disposing font package: {0}' -f $FontPackage.PackUri) 410 | $FontPackage.Package.DeletePart([IO.Packaging.PackUriHelper]::GetPartUri($FontPackage.PackUri)) 411 | [IO.Packaging.PackageStore]::RemovePackage($FontPackage.PackageUri) 412 | $FontPackage.Package.Close() 413 | } 414 | 415 | Function Test-IsAdministrator { 416 | [CmdletBinding()] 417 | [OutputType([Boolean])] 418 | Param() 419 | 420 | $User = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent() 421 | if ($User.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { 422 | return $true 423 | } 424 | 425 | return $false 426 | } 427 | 428 | # Windows 10 1809 and Windows Server 2019 introduced support for installing 429 | # fonts per-user. The corresponding Windows release build number is 17763. 430 | Function Test-PerUserFontsSupported { 431 | [CmdletBinding()] 432 | [OutputType([Boolean])] 433 | Param() 434 | 435 | $BuildNumber = [Int](Get-CimInstance -ClassName 'Win32_OperatingSystem' -Verbose:$false).BuildNumber 436 | if ($BuildNumber -ge 17763) { 437 | return $true 438 | } 439 | 440 | return $false 441 | } 442 | 443 | # Validate the install scope and method 444 | if ($Scope -eq 'System') { 445 | if (!(Test-IsAdministrator) -and !$WhatIfPreference) { 446 | throw 'Administrator privileges are required to install system-wide fonts.' 447 | } elseif ($Method -eq 'Shell' -and (Test-PerUserFontsSupported)) { 448 | throw 'Installing fonts system-wide via the Shell API is unsupported from Windows 10 1809.' 449 | } 450 | } elseif (!(Test-PerUserFontsSupported)) { 451 | throw 'Per-user fonts are only supported from Windows 10 1809 and Windows Server 2019.' 452 | } 453 | 454 | # Use script location if no path provided 455 | if (!$Path) { 456 | $Path = $PSScriptRoot 457 | } 458 | 459 | # Validate the source font path 460 | try { 461 | $SourceFontPath = Get-Item -Path $Path -ErrorAction Stop 462 | } catch { 463 | throw 'Provided path is invalid: {0}' -f $Path 464 | } 465 | 466 | # Validate uninstall of existing font 467 | if ($UninstallExisting) { 468 | if ($Method -ne 'Manual') { 469 | throw 'The -UninstallExisting parameter can only be used with the Manual install method.' 470 | } 471 | 472 | $UninstallFont = 'Uninstall-Font' 473 | if (!(Get-Command -Name $UninstallFont -ErrorAction Ignore)) { 474 | $UninstallFont = Join-Path -Path $PSScriptRoot -ChildPath 'Uninstall-Font.ps1' 475 | if (!(Test-Path -Path $UninstallFont -PathType Leaf)) { 476 | throw 'Unable to locate Uninstall-Font.ps1 script required for -UninstallExisting use.' 477 | } 478 | } 479 | } 480 | 481 | # Enumerate fonts to be installed 482 | if ($SourceFontPath -is [IO.DirectoryInfo]) { 483 | $SourceFonts = @(Get-ChildItem -Path $SourceFontPath | Where-Object Extension -In $ValidExts) 484 | 485 | if (!$SourceFonts) { 486 | throw 'Unable to locate any fonts in provided directory: {0}' -f $SourceFontPath 487 | } 488 | } elseif ($SourceFontPath -is [IO.FileInfo]) { 489 | if ($SourceFontPath.Extension -notin $ValidExts) { 490 | throw 'Provided file does not appear to be a valid font: {0}' -f $SourceFontPath 491 | } 492 | 493 | $SourceFonts = @($SourceFontPath) 494 | } else { 495 | throw 'Expected directory or file but received: {0}' -f $SourceFontPath.GetType().Name 496 | } 497 | 498 | # Retrieve installed fonts 499 | $InstalledFonts = Get-Fonts -Scope $Scope 500 | 501 | # Calculate the hash of each installed font 502 | foreach ($Font in $InstalledFonts) { 503 | $FontHash = Get-FileHash -Path $Font.File.FullName 504 | $Font | Add-Member -MemberType NoteProperty -Name 'Hash' -Value $FontHash.Hash 505 | } 506 | 507 | # Filter out any already installed fonts 508 | $InstallFonts = New-Object -TypeName 'Collections.Generic.List[IO.FileInfo]' 509 | foreach ($Font in $SourceFonts) { 510 | if ($InstalledFonts.Count -eq 0) { 511 | $InstallFonts.Add($Font) 512 | continue 513 | } 514 | 515 | $FontHash = Get-FileHash -Path $Font.FullName 516 | if ($FontHash.Hash -notin $InstalledFonts.Hash) { 517 | $InstallFonts.Add($Font) 518 | } else { 519 | Write-Verbose -Message ('Font is already installed: {0}' -f $Font.Name) 520 | } 521 | } 522 | 523 | # Install fonts using selected method 524 | if ($InstallFonts) { 525 | switch ($Method) { 526 | 'Manual' { Install-FontManual -Fonts $InstallFonts -Scope $Scope -UninstallExisting:$UninstallExisting } 527 | 'Shell' { Install-FontShell -Fonts $InstallFonts } 528 | } 529 | } 530 | -------------------------------------------------------------------------------- /Scripts/Install-VSTOAddin.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Install a Visual Studio Tools for Office (VSTO) add-in 4 | 5 | .DESCRIPTION 6 | Launches the installation of a VSTO add-in. 7 | 8 | .PARAMETER ManifestPath 9 | Path to the manifest file for the VSTO add-in. 10 | 11 | The path can be any of: 12 | - A path on the local computer 13 | - A path to a UNC file share 14 | - A path to a HTTP(S) site 15 | 16 | .PARAMETER Silent 17 | Runs the VSTO operation silently. 18 | 19 | .EXAMPLE 20 | Install-VSTOAddin -ManifestPath 'https://example.s3.amazonaws.com/live/MyAddin.vsto' -Silent 21 | 22 | Silently installs the VSTO add-in described by the provided manifest. 23 | 24 | .NOTES 25 | The Visual Studio 2010 Tools for Office Runtime, which includes VSTOInstaller.exe, must be present on the system. 26 | 27 | Silent mode requires the certificate with which the manifest is signed to be present in the Trusted Publishers certificate store. 28 | 29 | The VSTOInstaller.exe which matches the bitness of the PowerShell runtime will be used (e.g. 64-bit VSTOInstaller.exe with 64-bit PowerShell). 30 | 31 | Parameters for VSTOInstaller.exe 32 | https://learn.microsoft.com/en-us/previous-versions/visualstudio/visual-studio-2010/bb772078(v=vs.100)#parameters-for-vstoinstallerexe 33 | 34 | VSTOInstaller Error Codes 35 | https://learn.microsoft.com/en-us/previous-versions/visualstudio/visual-studio-2010/bb757423(v=vs.100) 36 | 37 | .LINK 38 | https://github.com/ralish/PSWinGlue 39 | #> 40 | 41 | #Requires -Version 3.0 42 | 43 | [CmdletBinding()] 44 | [OutputType([Void])] 45 | Param( 46 | [Parameter(Mandatory)] 47 | [String]$ManifestPath, 48 | 49 | [Switch]$Silent 50 | ) 51 | 52 | $PowerShellCore = New-Object -TypeName Version -ArgumentList 6, 0 53 | if ($PSVersionTable.PSVersion -ge $PowerShellCore -and $PSVersionTable.Platform -ne 'Win32NT') { 54 | throw '{0} is only compatible with Windows.' -f $MyInvocation.MyCommand.Name 55 | } 56 | 57 | $VstoInstaller = '{0}\Microsoft Shared\VSTO\10.0\VSTOInstaller.exe' -f $env:CommonProgramFiles 58 | $VstoArguments = @('/Install', ('"{0}"' -f $ManifestPath)) 59 | 60 | if ($Silent) { 61 | $VstoArguments += '/Silent' 62 | } 63 | 64 | if (!(Test-Path -Path $VstoInstaller)) { 65 | throw 'VSTO Installer not present at: {0}' -f $VstoInstaller 66 | } 67 | 68 | Write-Verbose -Message ('Installing VSTO add-in with arguments: {0}', [String]::Join(' ', $VstoArguments)) 69 | $VstoProcess = Start-Process -FilePath $VstoInstaller -ArgumentList $VstoArguments -Wait -PassThru 70 | if ($VstoProcess.ExitCode -ne 0) { 71 | throw 'VSTO Installer failed with code: {0}' -f $VstoProcess.ExitCode 72 | } 73 | -------------------------------------------------------------------------------- /Scripts/Register-MicrosoftUpdate.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Register the Microsoft Update service with the Windows Update Agent 4 | 5 | .DESCRIPTION 6 | The Windows Update Agent (WUA) has a concept of "services" which, once registered with WUA, add additional catalogues of updates for delivery to the system. 7 | 8 | By default, the Windows Update service is registered to provide updates to the operating system. To receive updates for other installed Microsoft products the Microsoft Update service must be registered. 9 | 10 | This function automates the registration of the Microsoft Update service with WUA to ensure the operating system and all installed Microsoft products receive available updates. 11 | 12 | .EXAMPLE 13 | Register-MicrosoftUpdate 14 | 15 | Ensures the Microsoft Update service is registered with the Windows Update Agent. 16 | 17 | .NOTES 18 | Administrator privileges are required to modify the Windows Update Agent configuration. 19 | 20 | .LINK 21 | https://github.com/ralish/PSWinGlue 22 | #> 23 | 24 | #Requires -Version 3.0 25 | 26 | [CmdletBinding()] 27 | [OutputType([Void])] 28 | Param() 29 | 30 | $PowerShellCore = New-Object -TypeName Version -ArgumentList 6, 0 31 | if ($PSVersionTable.PSVersion -ge $PowerShellCore -and $PSVersionTable.Platform -ne 'Win32NT') { 32 | throw '{0} is only compatible with Windows.' -f $MyInvocation.MyCommand.Name 33 | } 34 | 35 | $User = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent() 36 | if (!$User.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { 37 | throw '{0} requires Administrator privileges.' -f $MyInvocation.MyCommand.Name 38 | } 39 | 40 | # Opt-In to Microsoft Update 41 | # https://learn.microsoft.com/en-us/windows/win32/wua_sdk/opt-in-to-microsoft-update 42 | $MuServiceId = '7971f918-a847-4430-9279-4a52d1efe18d' 43 | 44 | # AddServiceFlag enumeration 45 | # https://learn.microsoft.com/en-us/windows/win32/api/wuapi/ne-wuapi-addserviceflag 46 | $asfAllowPendingRegistration = 1 47 | $asfAllowOnlineRegistration = 2 48 | $asfRegisterServiceWithAU = 4 49 | $MuServiceFlags = $asfAllowPendingRegistration + $asfAllowOnlineRegistration + $asfRegisterServiceWithAU 50 | 51 | $WuaServiceManager = $null 52 | $WuaServices = $null 53 | $MuService = $null 54 | 55 | try { 56 | $WuaServiceManager = New-Object -ComObject 'Microsoft.Update.ServiceManager' 57 | 58 | $MuServiceRegistered = $false 59 | $WuaServices = $WuaServiceManager.Services 60 | for ($i = 0; $i -lt $WuaServices.Count; $i++) { 61 | $WuaService = $WuaServices.Item($i) 62 | 63 | if ($WuaService.ServiceID -contains $MuServiceId) { 64 | $MuServiceRegistered = $true 65 | } 66 | 67 | $null = [Runtime.InteropServices.Marshal]::ReleaseComObject($WuaService) 68 | 69 | if ($MuServiceRegistered) { 70 | Write-Verbose -Message 'Microsoft Update service already registered with WUA.' 71 | return 72 | } 73 | } 74 | 75 | Write-Verbose -Message 'Registering Microsoft Update service with WUA ...' 76 | $MuService = $WuaServiceManager.AddService2($MuServiceId, $MuServiceFlags, '') 77 | } catch { 78 | throw $_ 79 | } finally { 80 | if ($MuService) { $null = [Runtime.InteropServices.Marshal]::ReleaseComObject($MuService) } 81 | if ($WuaServices) { $null = [Runtime.InteropServices.Marshal]::ReleaseComObject($WuaServices) } 82 | if ($WuaServiceManager) { $null = [Runtime.InteropServices.Marshal]::ReleaseComObject($WuaServiceManager) } 83 | } 84 | -------------------------------------------------------------------------------- /Scripts/Remove-AlternateDataStream.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Remove common unwanted alternate data streams from files 4 | 5 | .DESCRIPTION 6 | Alternate data streams can contain important (meta)data which shouldn't be removed, however, they can also be a nuisance. 7 | 8 | This is particularly true of certain common alternate data streams used to perform additional prompting of the user for "untrusted" files. 9 | 10 | This cmdlet provides options to remove common alternate data streams which may be unwanted while preserving any other alternate data streams. 11 | 12 | .PARAMETER Dropbox 13 | Remove alternate data streams added by Dropbox. 14 | 15 | These data streams are not publicly documented but appear to at least contain a unique machine identifier for tracking purposes. 16 | 17 | .PARAMETER Path 18 | Directory from which to remove specified alternate data streams from files. 19 | 20 | .PARAMETER Recurse 21 | Recurse into subdirectories. 22 | 23 | .PARAMETER ZoneIdentifier 24 | Remove the Zone Identifier alternate data stream. 25 | 26 | This stream indicates the origin of a downloaded file and is typically used to trigger additional prompts or protections on opening "untrusted" files. 27 | 28 | .EXAMPLE 29 | Remove-AlternateDataStreams -Path 'D:\Library' -ZoneIdentifier -Recurse 30 | 31 | Removes all Zone.Identifier alternate data streams recursively from files in "D:\Library". 32 | 33 | .NOTES 34 | For bulk removal of alternate data streams consider Sysinternals Streams: 35 | https://learn.microsoft.com/en-us/sysinternals/downloads/streams 36 | 37 | .LINK 38 | https://github.com/ralish/PSWinGlue 39 | #> 40 | 41 | #Requires -Version 3.0 42 | 43 | [CmdletBinding(SupportsShouldProcess)] 44 | [OutputType([Void])] 45 | Param( 46 | [Parameter(Mandatory)] 47 | [String]$Path, 48 | 49 | [Switch]$ZoneIdentifier, 50 | [Switch]$Dropbox, 51 | 52 | [Switch]$Recurse 53 | ) 54 | 55 | $PowerShellCore = New-Object -TypeName Version -ArgumentList 6, 0 56 | if ($PSVersionTable.PSVersion -ge $PowerShellCore -and $PSVersionTable.Platform -ne 'Win32NT') { 57 | throw '{0} is only compatible with Windows.' -f $MyInvocation.MyCommand.Name 58 | } 59 | 60 | try { 61 | $AdsPath = Get-Item -Path $Path 62 | } catch { 63 | throw 'Provided path is invalid: {0}' -f $Path 64 | } 65 | 66 | if ($AdsPath -isnot [IO.DirectoryInfo]) { 67 | throw 'Expected directory but received: {0}' -f $AdsPath.GetType().Name 68 | } 69 | 70 | $Streams = @() 71 | 72 | if ($ZoneIdentifier) { 73 | $Streams += 'Zone.Identifier' 74 | } 75 | 76 | if ($Dropbox) { 77 | $Streams += 'com.dropbox.attrs' 78 | $Streams += 'com.dropbox.attributes' 79 | } 80 | 81 | if (!$Streams) { 82 | throw 'No alternate data streams to remove were specified.' 83 | } 84 | 85 | Get-ChildItem -Path $AdsPath -Recurse:$Recurse -File | ForEach-Object { 86 | try { 87 | $_ | Remove-Item -Stream $Streams 88 | } catch { 89 | switch -Regex ($PSItem.FullyQualifiedErrorId) { 90 | '^AlternateDataStreamNotFound,' { } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Scripts/Remove-OrphanDependencyPackages.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Removes orphan dependency packages in the system package cache 4 | 5 | .DESCRIPTION 6 | Windows maintains a local package cache of installed software to simplify operations which require access to the original installer. The package cache is typically located at "C:\ProgramData\Package Cache". 7 | 8 | Not all installers use the package cache, but Windows Installer (MSI files) typically do, as without a cached copy of the installer it's not possible to modify or even remove the existing installation. 9 | 10 | Unfortunately, the package cache may in some cases maintain installers for removed applications, growing in size as old installers are accumulated. This is especially the case for dependency packages. 11 | 12 | Visual Studio is an particularly prominent offender, as it relies on many MSIs which are frequently updated, but not always cleanly removed. The various .NET Core packages are the most common examples. 13 | 14 | This function can uninstall "orphaned" dependency packages and remove cached installers, as identified by the Find-OrphanDependencyPackages function. You use this function entirely at your own risk! 15 | 16 | .EXAMPLE 17 | $Packages = Find-OrphanDependencyPackages 18 | Remove-OrphanDependencyPackages -Packages $Packages 19 | 20 | Removes orphan dependency packages found by Find-OrphanDependencyPackages. 21 | 22 | .NOTES 23 | Removal of an orphaned dependency package consists of several steps: 24 | - If a cached MSI installer was identified, invoke "msiexec" requesting it be uninstalled. 25 | - Remove the folder in the package cache which contains the installer. 26 | - Remove the registry data for the orphaned package. 27 | 28 | This function supports the "-WhatIf" parameter and you are *strongly* encouraged to use it to first verify what it's going to do. This can be done without Administrator rights for additional protection. 29 | 30 | .LINK 31 | https://github.com/ralish/PSWinGlue 32 | #> 33 | 34 | #Requires -Version 3.0 35 | 36 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')] 37 | [CmdletBinding(SupportsShouldProcess)] 38 | [OutputType([Void])] 39 | Param( 40 | [Parameter(Mandatory)] 41 | [PSCustomObject[]]$Packages 42 | ) 43 | 44 | $PowerShellCore = New-Object -TypeName Version -ArgumentList 6, 0 45 | if ($PSVersionTable.PSVersion -ge $PowerShellCore -and $PSVersionTable.Platform -ne 'Win32NT') { 46 | throw '{0} is only compatible with Windows.' -f $MyInvocation.MyCommand.Name 47 | } 48 | 49 | Function Test-IsAdministrator { 50 | [CmdletBinding()] 51 | [OutputType([Boolean])] 52 | Param() 53 | 54 | $User = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent() 55 | if ($User.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { 56 | return $true 57 | } 58 | 59 | return $false 60 | } 61 | 62 | Function Test-PackageObjectProperties { 63 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] 64 | [CmdletBinding()] 65 | [OutputType([Boolean])] 66 | Param( 67 | [Parameter(Mandatory)] 68 | [PSCustomObject]$Package 69 | ) 70 | 71 | $ExpectedProperties = 'Name', 'Status', 'RegKey', 'CacheStatus', 'CacheFiles' 72 | foreach ($Property in $ExpectedProperties) { 73 | if ($Package.PSObject.Properties.Name -notcontains $Property) { 74 | throw 'Encounted a package with a missing required property: {0}' -f $Property 75 | } 76 | } 77 | 78 | if ($Package.Status -ne 'Orphaned') { 79 | return $false 80 | } 81 | 82 | if ($Package.CacheFiles.Count -gt 1) { 83 | Write-Warning -Message ('[{0}] Skipping package with multiple files.' -f $Package.Name) 84 | return $false 85 | } 86 | 87 | # Remaining checks don't apply 88 | if ($Package.CacheFiles.Count -eq 0) { 89 | return $true 90 | } 91 | 92 | if (!$Package.CacheFiles[0].Name.EndsWith('.msi')) { 93 | Write-Error -Message ('[{0}] Skipping package without MSI installer.' -f $Package.Name) 94 | return $false 95 | } 96 | 97 | return $true 98 | } 99 | 100 | if (!(Test-IsAdministrator) -and !$WhatIfPreference) { 101 | throw '{0} requires Administrator privileges.' -f $MyInvocation.MyCommand.Name 102 | } 103 | 104 | $MaxPackageNameSize = 0 105 | foreach ($Package in $Packages) { 106 | if ($Package.Name.Length -gt $MaxPackageNameSize) { 107 | $MaxPackageNameSize = $Package.Name.Length 108 | } 109 | } 110 | # Negative for left justification and -2 to account for added brackets 111 | $MaxPackageNameSize = ($MaxPackageNameSize / -1) - 2 112 | 113 | foreach ($Package in $Packages) { 114 | if (!(Test-PackageObjectProperties -Package $Package)) { 115 | continue 116 | } 117 | 118 | $PackageNameBracketed = '[{0}]' -f $Package.Name 119 | $PackageOutputPrefix = "{0,$MaxPackageNameSize}" -f $PackageNameBracketed 120 | Write-Verbose -Message ('{0} Processing package ...' -f $PackageOutputPrefix) 121 | 122 | if ($Package.CacheFiles.Count -eq 1) { 123 | $MsiexecParams = @( 124 | '/u' 125 | ('"{0}"' -f $Package.CacheFiles[0].FullName) 126 | '/passive' 127 | ) 128 | 129 | Start-Process -FilePath 'msiexec.exe' -ArgumentList $MsiexecParams -Wait 130 | if ($LASTEXITCODE -ne 0) { 131 | Write-Warning -Message ('{0} Uninstall returned exit code: {1}' -f $PackageOutputPrefix, $LASTEXITCODE) 132 | continue 133 | } 134 | 135 | Remove-Item -LiteralPath $Package.CacheFiles[0].Directory -Recurse 136 | } 137 | 138 | # Calling Remove-Item with -WhatIf on a registry key which will require 139 | # Administrator privileges to remove won't bypass the permissions check. 140 | # Simulate what would be returned if we did have required privileges so an 141 | # unhelpful "Requested registry access is not allowed." error is not shown. 142 | if ($WhatIfPreference) { 143 | $Key = Get-Item -LiteralPath $Package.RegKey 144 | Write-Host -Object ('What if: Performing the operation "Remove Key" on target "Item: {0}".' -f $Key.Name) 145 | } else { 146 | Remove-Item -LiteralPath $Package.RegKey -Recurse 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /Scripts/Set-SharedPCMode.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Configures Shared PC Mode using the SharedPC CSP via the MDM Bridge WMI Provider 4 | 5 | .DESCRIPTION 6 | The typical approach for configuring Shared PC Mode is to use an MDM solution which interacts with the SharedPC CSP. 7 | 8 | Alternatively, configuration can be performed by using the MDM Bridge WMI Provider to interact with the CSP via WMI. 9 | 10 | This function eases configuration when using the latter approach by providing a simple command to set available settings. 11 | 12 | For details on parameters consult the Shared PC Mode documentation (see the NOTES section of this command for links). 13 | 14 | .PARAMETER PassThru 15 | Return an instance of the MDM_SharedPC class after applying the requested configuration. 16 | 17 | .EXAMPLE 18 | Set-SharedPCMode -EnableSharedPCMode $true -EnableAccountManager $true -RestrictLocalStorage $true 19 | 20 | Enables Shared PC Mode with automatic account management and local storage restrictions. 21 | 22 | .NOTES 23 | VPN configuration using the VPNv2 CSP is only available on Windows 10 1607 or later. 24 | 25 | To interact with the MDM Bridge WMI Provider the function must be running as SYSTEM. 26 | 27 | Typically this function would be run non-interactively by a service running in the SYSTEM context (e.g. Group Policy Client). 28 | 29 | To run this function interactively you should use a tool like Sysinternals PsExec to run it under the SYSTEM account. 30 | 31 | For example, the following PsExec command will launch PowerShell under the SYSTEM account: psexec -s -i powershell 32 | 33 | Set up a shared or guest PC with Windows 10/11 34 | https://learn.microsoft.com/en-us/windows/configuration/set-up-shared-or-guest-pc 35 | 36 | SharedPC CSP 37 | https://learn.microsoft.com/en-us/windows/client-management/mdm/sharedpc-csp 38 | 39 | MDM_SharedPC class 40 | https://learn.microsoft.com/en-us/windows/win32/dmwmibridgeprov/mdm-sharedpc 41 | 42 | .LINK 43 | https://github.com/ralish/PSWinGlue 44 | #> 45 | 46 | # Minimum supported Windows release ships with PowerShell 5.1 47 | #Requires -Version 5.1 48 | 49 | [CmdletBinding()] 50 | [OutputType([Void], [Microsoft.Management.Infrastructure.CimInstance])] 51 | Param( 52 | [Bool]$EnableSharedPCMode, 53 | [Bool]$EnableSharedPCModeWithOneDriveSync, 54 | [Bool]$EnableWindowsInsiderPreviewFlighting, 55 | [Bool]$SetEduPolicies, 56 | [Bool]$SetPowerPolicies, 57 | [Int]$MaintenanceStartTime, 58 | [Bool]$SignInOnResume, 59 | [Int]$SleepTimeout, 60 | [Bool]$EnableAccountManager, 61 | [Int]$AccountModel, 62 | [Int]$DeletionPolicy, 63 | [Int]$DiskLevelDeletion, 64 | [Int]$DiskLevelCaching, 65 | [Bool]$RestrictLocalStorage, 66 | [String]$KioskModeAUMID, 67 | [String]$KioskModeUserTileDisplayText, 68 | [Int]$InactiveThreshold, 69 | [Int]$MaxPageFileSizeMB, 70 | 71 | [Switch]$PassThru 72 | ) 73 | 74 | $WmiNamespace = 'root\cimv2\mdm\dmmap' 75 | $WmiClassName = 'MDM_SharedPC' 76 | 77 | $OSRequiredType = 1 # Workstation 78 | $OSRequiredBuild = 14393 # Windows 10 1607 79 | $SidSystem = 'S-1-5-18' # NT AUTHORITY\SYSTEM 80 | 81 | $PowerShellCore = New-Object -TypeName Version -ArgumentList 6, 0 82 | if ($PSVersionTable.PSVersion -ge $PowerShellCore -and $PSVersionTable.Platform -ne 'Win32NT') { 83 | throw '{0} is only compatible with Windows.' -f $MyInvocation.MyCommand.Name 84 | } 85 | 86 | $OSCurrentType = (Get-CimInstance -ClassName 'Win32_OperatingSystem' -Verbose:$false).ProductType 87 | $OSCurrentBuild = [Environment]::OSVersion.Version.Build 88 | if ($OSCurrentBuild -lt $OSRequiredBuild -or $OSCurrentType -ne $OSRequiredType) { 89 | throw 'Shared PC Mode is only available on Windows 10 1607 or later.' 90 | } 91 | 92 | $PowerShellMin = New-Object -TypeName Version -ArgumentList 5, 1 93 | if ($PSVersionTable.PSVersion -lt $PowerShellMin) { 94 | throw '{0} requires at least PowerShell {1}.' -f $MyInvocation.MyCommand.Name, $PowerShellMin 95 | } 96 | 97 | $SidCurrent = ([Security.Principal.WindowsIdentity]::GetCurrent()).User.Value 98 | if ($SidCurrent -ne $SidSystem) { 99 | throw 'Must be running as SYSTEM to interact with MDM Bridge WMI Provider.' 100 | } 101 | 102 | $MdmSharedPC = Get-CimInstance -Namespace $WmiNamespace -ClassName $WmiClassName -ErrorAction Stop 103 | $MdmCspProperties = $MdmSharedPC.get_CimInstanceProperties().Name 104 | $IgnoredParameters = [Management.Automation.Cmdlet]::CommonParameters + 'PassThru' 105 | 106 | foreach ($Parameter in $PSBoundParameters.GetEnumerator()) { 107 | if ($Parameter.Key -notin $IgnoredParameters) { 108 | if ($Parameter.Key -notin $MdmCspProperties) { 109 | throw 'Parameter not supported on this Windows version: {0}' -f $Parameter.Key 110 | } 111 | 112 | $MdmSharedPC.($Parameter.Key) = $Parameter.Value 113 | } 114 | } 115 | 116 | Set-CimInstance -CimInstance $MdmSharedPC -ErrorAction Stop 117 | 118 | if ($PassThru) { 119 | Get-CimInstance -Namespace $WmiNamespace -ClassName $WmiClassName 120 | } 121 | -------------------------------------------------------------------------------- /Scripts/Sort-RegistryExport.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Lexically sorts the exported values for each registry key in a Windows Registry export 4 | 5 | .DESCRIPTION 6 | Exporting a registry key to a ".reg" file will typically not result in the exported values being lexically sorted. 7 | 8 | This command sorts the exported values under each registry key in a Windows Registry export in lexicographical order. 9 | 10 | .PARAMETER Path 11 | The path to a Windows Registry export which will have the values for each exported registry key sorted lexically. 12 | 13 | .EXAMPLE 14 | Sort-Registryexport -Path Export.reg 15 | 16 | Sorts the registry values lexically for each exported registry key in the Export.reg file. 17 | 18 | .NOTES 19 | Windows Registry export are expected to have a first line beginning with "Windows Registry Editor". 20 | 21 | Registry keys are not sorted (only their values), however, Windows built-in tools export keys in lexical order. 22 | 23 | .LINK 24 | https://github.com/ralish/PSWinGlue 25 | #> 26 | 27 | #Requires -Version 3.0 28 | 29 | [CmdletBinding()] 30 | [OutputType([Void])] 31 | Param( 32 | [Parameter(Mandatory)] 33 | [String]$Path 34 | ) 35 | 36 | $RegSignature = 'Windows Registry Editor' 37 | 38 | try { 39 | $RegFile = Get-Item -Path $Path -ErrorAction Stop 40 | } catch { 41 | throw $_ 42 | } 43 | 44 | if ($RegFile -isnot [IO.FileInfo]) { 45 | throw 'Expected a file but received: {0}' -f $RegFile.GetType().Name 46 | } 47 | 48 | $RegFileSig = Get-Content -Path $RegFile -TotalCount 1 49 | if ($RegFileSig -notmatch "^$RegSignature") { 50 | throw 'File does not begin with expected "{0}" signature: {1}' -f $RegSignature, $RegFile.Name 51 | } 52 | 53 | # Everything looks good so retrieve the complete file content 54 | $RegFileContent = Get-Content -Path $RegFile 55 | 56 | # List to hold the new file content starting with the signature 57 | $RegNewContent = New-Object -TypeName 'Collections.Generic.List[String]' 58 | $RegNewContent.Add($RegFileContent[0]) 59 | 60 | # List holding entries for the current registry key (INI section) 61 | $RegKeyContent = New-Object -TypeName 'Collections.Generic.List[String]' 62 | 63 | for ($Idx = 1; $Idx -lt $RegFileContent.Count; $Idx++) { 64 | $Line = $RegFileContent[$Idx] 65 | 66 | # Blank line (ignored) 67 | if ([String]::IsNullOrWhiteSpace($Line)) { 68 | continue 69 | } 70 | 71 | # Registry key 72 | if ($Line -match '^\[') { 73 | # Sort and append entries from the previous registry key 74 | if ($RegKeyContent.Count -ne 0) { 75 | $RegKeyContent.Sort() 76 | foreach ($Entry in $RegKeyContent) { 77 | $RegNewContent.Add($Entry) 78 | } 79 | 80 | $RegKeyContent.Clear() 81 | } 82 | 83 | $RegNewContent.Add([String]::Empty) 84 | $RegNewContent.Add($Line) 85 | continue 86 | } 87 | 88 | # Registry value 89 | if ($Line -match '^[@"]') { 90 | # Handle values where the data is split over multiple lines 91 | if ($Line -match '\\$') { 92 | do { 93 | $Idx++ 94 | $ExtraLine = $RegFileContent[$Idx] 95 | $Line += '{0}{1}' -f [Environment]::NewLine, $ExtraLine 96 | } while ($ExtraLine -match '\\$') 97 | } 98 | 99 | $RegKeyContent.Add($Line) 100 | continue 101 | } 102 | 103 | throw 'Unexpected content on line {0} sorting registry file: {1}' -f ($Idx + 1), $RegFile.Name 104 | } 105 | 106 | # Add any values from the final registry key 107 | if ($RegKeyContent.Count -ne 0) { 108 | $RegKeyContent.Sort() 109 | foreach ($Entry in $RegKeyContent) { 110 | $RegNewContent.Add($Entry) 111 | } 112 | } 113 | 114 | Set-Content -Path $RegFile -Value $RegNewContent 115 | -------------------------------------------------------------------------------- /Scripts/Uninstall-Font.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Uninstalls a specific font by name 4 | 5 | .DESCRIPTION 6 | Provides suport for uninstalling fonts by name. 7 | 8 | If the font is in-use and Administrator privileges are present, Windows will be configured to remove it next reboot. 9 | 10 | .PARAMETER Name 11 | The name of the font to be uninstalled, as per the font metadata. 12 | 13 | .PARAMETER Scope 14 | Specifies whether to uninstall the font from the system-wide or per-user fonts directory. 15 | 16 | Support for per-user fonts is only available from Windows 10 1809 and Windows Server 2019. 17 | 18 | Uninstalling system-wide fonts requires Administrator privileges. 19 | 20 | The default is system-wide. 21 | 22 | .Parameter IgnoreNotPresent 23 | If the font to be uninstalled is not registered, ignore it instead of throwing an exception. 24 | 25 | .EXAMPLE 26 | Uninstall-Font -Name 'Georgia (TrueType)' 27 | 28 | Uninstalls the "Georgia (TrueType)" font from the system-wide fonts directory. 29 | 30 | .NOTES 31 | Per-user fonts are only uninstalled in the context of the user executing the function. 32 | 33 | .LINK 34 | https://github.com/ralish/PSWinGlue 35 | #> 36 | 37 | #Requires -Version 3.0 38 | 39 | [CmdletBinding(SupportsShouldProcess)] 40 | [OutputType([Void])] 41 | Param( 42 | [Parameter(Mandatory)] 43 | [String]$Name, 44 | 45 | [ValidateSet('System', 'User')] 46 | [String]$Scope = 'System', 47 | 48 | [Switch]$IgnoreNotPresent 49 | ) 50 | 51 | $PowerShellCore = New-Object -TypeName Version -ArgumentList 6, 0 52 | if ($PSVersionTable.PSVersion -ge $PowerShellCore -and $PSVersionTable.Platform -ne 'Win32NT') { 53 | throw '{0} is only compatible with Windows.' -f $MyInvocation.MyCommand.Name 54 | } 55 | 56 | Function Uninstall-Font { 57 | [CmdletBinding(SupportsShouldProcess)] 58 | [OutputType([Void])] 59 | Param( 60 | [Parameter(Mandatory)] 61 | [String]$Name, 62 | 63 | [ValidateSet('System', 'User')] 64 | [String]$Scope = 'System', 65 | 66 | [Switch]$IgnoreNotPresent 67 | ) 68 | 69 | switch ($Scope) { 70 | 'System' { 71 | $FontsFolder = [Environment]::GetFolderPath('Fonts') 72 | $FontsRegKey = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts' 73 | } 74 | 75 | 'User' { 76 | $FontsFolder = Join-Path -Path ([Environment]::GetFolderPath('LocalApplicationData')) -ChildPath 'Microsoft\Windows\Fonts' 77 | $FontsRegKey = 'HKCU:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts' 78 | } 79 | } 80 | 81 | try { 82 | $FontsReg = Get-Item -Path $FontsRegKey -ErrorAction Stop 83 | } catch { 84 | throw 'Unable to open {0} fonts registry key: {1}' -f $Scope.ToLower(), $FontsRegKey 85 | } 86 | 87 | if ($FontsReg.Property -notcontains $Name) { 88 | if ($IgnoreNotPresent) { return } 89 | throw 'Font not registered for {0}: {1}' -f $Scope.ToLower(), $Name 90 | } 91 | 92 | $FontRegValue = $FontsReg.GetValue($Name) 93 | if ($Scope -eq 'User') { 94 | $FontFilePath = $FontRegValue 95 | } else { 96 | $FontFilePath = Join-Path -Path $FontsFolder -ChildPath $FontRegValue 97 | } 98 | 99 | if ($PSCmdlet.ShouldProcess($Name, 'Uninstall font')) { 100 | $RemoveOnReboot = $false 101 | 102 | try { 103 | Write-Debug -Message ('Removing font file: {0}' -f $FontFilePath) 104 | Remove-Item -Path $FontFilePath -ErrorAction Stop 105 | } catch [Management.Automation.ItemNotFoundException] { 106 | Write-Warning -Message ('Font file not found: {0}' -f $FontFilePath) 107 | } catch [UnauthorizedAccessException] { 108 | $RemoveOnReboot = $true 109 | } 110 | 111 | if ($RemoveOnReboot) { 112 | if (!(Test-IsAdministrator)) { 113 | throw 'Unable to uninstall in-use font. Retry as Administrator to remove on next reboot.' 114 | } 115 | 116 | if (!('PSWinGlue.UninstallFont' -as [Type])) { 117 | $MoveFileEx = @' 118 | [DllImport("kernel32.dll", CharSet = CharSet.Unicode, EntryPoint = "MoveFileExW", ExactSpelling = true, SetLastError = true)] 119 | public static extern bool MoveFileEx([MarshalAs(UnmanagedType.LPWStr)] string lpExistingFileName, IntPtr lpNewFileName, uint dwFlags); 120 | '@ 121 | 122 | Add-Type -Namespace 'PSWinGlue' -Name 'UninstallFont' -MemberDefinition $MoveFileEx 123 | } 124 | 125 | $MOVEFILE_DELAY_UNTIL_REBOOT = 4 126 | if ([PSWinGlue.UninstallFont]::MoveFileEx($FontFilePath, [IntPtr]::Zero, $MOVEFILE_DELAY_UNTIL_REBOOT) -eq $false) { 127 | throw (New-Object -TypeName 'ComponentModel.Win32Exception') 128 | } 129 | } 130 | 131 | Write-Debug -Message ('Removing font from {0} registry: {1}' -f $Scope.ToLower(), $Name) 132 | Remove-ItemProperty -Path $FontsRegKey -Name $Name 133 | } 134 | } 135 | 136 | Function Test-IsAdministrator { 137 | [CmdletBinding()] 138 | [OutputType([Boolean])] 139 | Param() 140 | 141 | $User = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent() 142 | if ($User.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { 143 | return $true 144 | } 145 | 146 | return $false 147 | } 148 | 149 | # Windows 10 1809 and Windows Server 2019 introduced support for installing 150 | # fonts per-user. The corresponding Windows release build number is 17763. 151 | Function Test-PerUserFontsSupported { 152 | [CmdletBinding()] 153 | [OutputType([Boolean])] 154 | Param() 155 | 156 | $BuildNumber = [Int](Get-CimInstance -ClassName 'Win32_OperatingSystem' -Verbose:$false).BuildNumber 157 | if ($BuildNumber -ge 17763) { 158 | return $true 159 | } 160 | 161 | return $false 162 | } 163 | 164 | if ($Scope -eq 'System') { 165 | if (!(Test-IsAdministrator) -and !$WhatIfPreference) { 166 | throw 'Administrator privileges are required to uninstall system-wide fonts.' 167 | } 168 | } elseif ($Scope -eq 'User' -and !(Test-PerUserFontsSupported)) { 169 | throw 'Per-user fonts are only supported from Windows 10 1809 and Windows Server 2019.' 170 | } 171 | 172 | Uninstall-Font @PSBoundParameters 173 | -------------------------------------------------------------------------------- /Scripts/Uninstall-ObsoleteModule.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Uninstalls obsolete PowerShell modules 4 | 5 | .DESCRIPTION 6 | There's no simple way to uninstall obsolete PowerShell modules on a system, where "obsolete" refers to a module for which a newer version is installed. 7 | 8 | This function is designed to make the process of uninstalling old PowerShell modules as simple as possible, instead of a manual and tedious process. 9 | 10 | .PARAMETER Name 11 | The names of the modules to be uninstalled. 12 | 13 | If not specified, all obsolete modules will be uninstalled. 14 | 15 | .PARAMETER IncludeDscModules 16 | Include obsolete DSC (Desired State Configuration) modules during uninstallation. 17 | 18 | This switch is provided as a safety check, as it's not uncommon to have multiple versions of DSC modules installed which are being actively used. 19 | 20 | .PARAMETER ProgressParentId 21 | The ID of the progress bar displayed by the calling (parent) command. 22 | 23 | This optional parameter can be used to ensure the progress bar is reliably displayed as a child of the progress bar displayed by a calling command. 24 | 25 | .EXAMPLE 26 | Uninstall-ObsoleteModule 27 | 28 | Uninstalls all obsolete PowerShell modules on the system, excluding obsolete DSC modules. 29 | 30 | .NOTES 31 | Obsolete modules which are a dependency of other modules will not be uninstalled, provided they are referenced in the dependent module(s) manifest. 32 | 33 | This function relies on functionality provided by the PowerShellGet module. At least PowerShellGet v2 is required, but PowerShellGet v3 is in many areas substantially more performant. 34 | 35 | At the time of writing, PowerShellGet v3 is still in pre-release and is not at feature parity with PowerShellGet v2. For this reason it's recommended you install both, and this function will use whichever is best for the task at hand. 36 | 37 | Running this function without Administrator privileges will result in only obsolete modules installed in the per-user scope being uninstalled. Obsolete modules installed for all users will output a warning message requesting Administrator privileges. 38 | 39 | .LINK 40 | https://github.com/ralish/PSWinGlue 41 | #> 42 | 43 | #Requires -Version 3.0 44 | 45 | [CmdletBinding(SupportsShouldProcess)] 46 | [OutputType([Void])] 47 | Param( 48 | [ValidateNotNullOrEmpty()] 49 | [String[]]$Name, 50 | 51 | [Switch]$IncludeDscModules, 52 | 53 | [ValidateRange(-1, [Int]::MaxValue)] 54 | [Int]$ProgressParentId 55 | ) 56 | 57 | $PowerShellGet = @(Get-Module -Name 'PowerShellGet' -ListAvailable -Verbose:$false) 58 | if (!$PowerShellGet) { 59 | throw 'Required module not available: PowerShellGet' 60 | } 61 | 62 | $PsGetV3 = $false 63 | $PsGetLatest = $PowerShellGet | Sort-Object -Property 'Version' -Descending | Select-Object -First 1 64 | if ($PsGetLatest.Version.Major -ge 3) { 65 | $PsGetV3 = $true 66 | } elseif ($PsGetLatest.Version.Major -lt 2) { 67 | throw 'At least PowerShellGet v2 is required but found: {0}' -f $PsGetLatest.Version 68 | } 69 | Write-Verbose -Message ('Using PowerShellGet v{0}' -f $PsGetLatest.Version) 70 | 71 | # Not all platforms have DSC support as part of PowerShell itself 72 | $DscSupported = Get-Command -Name 'Get-DscResource' -ErrorAction Ignore 73 | if ($IncludeDscModules -and !$DscSupported) { 74 | throw 'Unable to enumerate DSC modules as Get-DscResource command unavailable.' 75 | } 76 | 77 | $WriteProgressParams = @{ 78 | Activity = 'Uninstalling obsolete PowerShell modules' 79 | } 80 | 81 | if ($PSBoundParameters.ContainsKey('ProgressParentId')) { 82 | $WriteProgressParams['ParentId'] = $ProgressParentId 83 | $WriteProgressParams['Id'] = $ProgressParentId + 1 84 | } 85 | 86 | $GetParams = @{} 87 | if ($Name) { 88 | $GetParams['Name'] = $Name 89 | } 90 | 91 | Write-Progress @WriteProgressParams -Status 'Enumerating installed modules' -PercentComplete 1 92 | if ($PsGetV3) { 93 | $InstalledModules = Get-PSResource -Verbose:$false @GetParams 94 | } else { 95 | $InstalledModules = Get-InstalledModule -Verbose:$false @GetParams 96 | } 97 | 98 | # Get-PSResource returns all module versions, while Get-InstalledModule only 99 | # returns the latest version, so this is only necessary for PsGet v2. 100 | $UniqueModules = @($InstalledModules.Name | Sort-Object -Unique) 101 | 102 | # Percentage of the total progress for updating modules 103 | $ProgressPercentUpdatesBase = 10 104 | $ProgressPercentUpdatesSection = 90 105 | 106 | if (!$IncludeDscModules -and $DscSupported) { 107 | Write-Progress @WriteProgressParams -Status 'Enumerating DSC modules for exclusion' -PercentComplete 5 108 | 109 | # Get-DscResource likes to output multiple progress bars but lacks the good 110 | # manners to clean them up. The result is a visual mess when when we've got 111 | # our own progress bars. 112 | $OriginalProgressPreference = $ProgressPreference 113 | Set-Variable -Name 'ProgressPreference' -Scope Global -Value SilentlyContinue -WhatIf:$false 114 | 115 | try { 116 | # Get-DscResource may output various errors, most often due to 117 | # duplicate resources. That's often the case with, for example, the 118 | # PackageManagement module being available in multiple locations. 119 | $DscModules = @(Get-DscResource -Module * -ErrorAction Ignore -Verbose:$false | Select-Object -ExpandProperty ModuleName -Unique) 120 | } finally { 121 | Set-Variable -Name 'ProgressPreference' -Scope Global -Value $OriginalProgressPreference -WhatIf:$false 122 | } 123 | } 124 | 125 | if (!$PsGetV3) { 126 | # Retrieve all installed modules (inc. all versions). We use this as an 127 | # optimisation to avoid calling Get-InstalledModule wherever possible. 128 | Write-Progress @WriteProgressParams -CurrentOperation 'Enumerating available modules' -PercentComplete 5 129 | $AvailableModules = Get-Module -ListAvailable -Verbose:$false @GetParams 130 | } 131 | 132 | # Uninstall obsolete modules compatible with PowerShellGet 133 | for ($ModuleIdx = 0; $ModuleIdx -lt $UniqueModules.Count; $ModuleIdx++) { 134 | $ModuleName = $UniqueModules[$ModuleIdx] 135 | 136 | if (!$IncludeDscModules -and $DscSupported -and $ModuleName -in $DscModules) { 137 | Write-Verbose -Message ('Skipping DSC module: {0}' -f $ModuleName) 138 | continue 139 | } 140 | 141 | # Retrieve all versions of the module 142 | if ($PsGetV3) { 143 | $AllVersions = @($InstalledModules | Where-Object Name -EQ $ModuleName) 144 | } else { 145 | # Try to avoid additional calls to Get-InstalledModule as it's *very* 146 | # slow. Unfortunately "Get-Module -ListAvailable" can't be relied on 147 | # due to a bug in older PowerShell releases. Affected releases won't 148 | # list modules with certain names if they aren't already imported. 149 | # 150 | # See: https://github.com/PowerShell/PowerShell/pull/8777 151 | $MatchingModules = @($AvailableModules | Where-Object Name -EQ $ModuleName) 152 | if ($MatchingModules -and $MatchingModules.Count -eq 1) { 153 | continue 154 | } 155 | 156 | $AllVersions = @(Get-InstalledModule -Name $ModuleName -AllVersions -Verbose:$false) 157 | } 158 | 159 | # Only a single version of the module appears to be installed 160 | if ($AllVersions.Count -eq 1) { 161 | continue 162 | } 163 | 164 | # A special case for PowerShellGet, at least while v3 is in beta: if a v2 165 | # version and a v3 version are installed side-by-side, retain the latest 166 | # version within each major release, instead of only the latest version. 167 | if ($ModuleName -eq 'PowerShellGet' -and $AllVersions.Version.Major -contains 2 -and $AllVersions.Version.Major -contains 3) { 168 | $ObsoletePsGet = @($AllVersions | Where-Object { $_.Version.Major -ne 2 -and $_.Version.Major -ne 3 }) 169 | 170 | $ObsoletePsGetV2 = @($AllVersions | Where-Object { $_.Version.Major -eq 2 } | Sort-Object -Property Version) 171 | if ($ObsoletePsGetV2.Count -gt 1) { 172 | $ObsoletePsGet += @($ObsoletePsGetV2[0..($ObsoletePsGetV2.Count - 2)]) 173 | } 174 | 175 | $ObsoletePsGetV3 = @($AllVersions | Where-Object { $_.Version.Major -eq 3 } | Sort-Object -Property Version) 176 | if ($ObsoletePsGetV3.Count -gt 1) { 177 | $ObsoletePsGet += @($ObsoletePsGetV3[0..($ObsoletePsGetV3.Count - 2)]) 178 | } 179 | 180 | $ObsoleteModules = @($ObsoletePsGet | Sort-Object -Property Version) 181 | } else { 182 | $SortedModules = @($AllVersions | Sort-Object -Property Version) 183 | $ObsoleteModules = @($SortedModules[0..($SortedModules.Count - 2)]) 184 | } 185 | 186 | $ObsoleteVersions = $ObsoleteModules.Version -join ', ' 187 | $ObsoleteVersionsWithModuleName = '{0}: {1}' -f $ModuleName, ($ObsoleteVersions -join ', ') 188 | 189 | $PercentComplete = ($ModuleIdx + 1) / $UniqueModules.Count * $ProgressPercentUpdatesSection + $ProgressPercentUpdatesBase 190 | $CurrentOperation = 'Uninstalling {0} version(s): {1}' -f $ModuleName, $ObsoleteVersions 191 | Write-Progress @WriteProgressParams -Status $CurrentOperation -PercentComplete $PercentComplete 192 | 193 | if ($PSCmdlet.ShouldProcess($ObsoleteVersionsWithModuleName, 'Uninstall obsolete versions')) { 194 | foreach ($ObsoleteModule in $ObsoleteModules) { 195 | try { 196 | if ($PsGetV3) { 197 | $ObsoleteModule | Uninstall-PSResource -ErrorAction Stop -Verbose:$false 198 | } else { 199 | $ObsoleteModule | Uninstall-Module -ErrorAction Stop -Verbose:$false 200 | } 201 | } catch { 202 | switch -Regex ($PSItem.FullyQualifiedErrorId) { 203 | '^AdminPrivilegesRequiredForUninstall,' { 204 | Write-Warning -Message ('Unable to uninstall module without Administrator rights: {0} v{1}' -f $ObsoleteModule.Name, $ObsoleteModule.Version) 205 | } 206 | 207 | # Uninstall-Module prints its own warning 208 | '^ModuleIsInUse,' { } 209 | 210 | '^(UnableToUninstallAsOtherModulesNeedThisModule|UninstallPSResourcePackageIsaDependency),' { 211 | Write-Warning -Message ('Unable to uninstall module due to presence of dependent modules: {0} v{1}' -f $ObsoleteModule.Name, $ObsoleteModule.Version) 212 | } 213 | 214 | Default { throw } 215 | } 216 | } 217 | } 218 | } 219 | } 220 | 221 | Write-Progress @WriteProgressParams -Completed 222 | -------------------------------------------------------------------------------- /Scripts/Uninstall-VSTOAddin.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Uninstall a Visual Studio Tools for Office (VSTO) add-in 4 | 5 | .DESCRIPTION 6 | Launches the uninstallation of a VSTO add-in. 7 | 8 | .PARAMETER ManifestPath 9 | Path to the manifest file for the VSTO add-in. 10 | 11 | The path can be any of: 12 | - A path on the local computer 13 | - A path to a UNC file share 14 | - A path to a HTTP(S) site 15 | 16 | .PARAMETER Silent 17 | Runs the VSTO operation silently. 18 | 19 | .EXAMPLE 20 | Uninstall-VSTOAddin -ManifestPath 'https://example.s3.amazonaws.com/live/MyAddin.vsto' -Silent 21 | 22 | Silently uninstalls the VSTO add-in described by the provided manifest. 23 | 24 | .NOTES 25 | The Visual Studio 2010 Tools for Office Runtime, which includes VSTOInstaller.exe, must be present on the system. 26 | 27 | Silent mode requires the certificate with which the manifest is signed to be present in the Trusted Publishers certificate store. 28 | 29 | The VSTOInstaller.exe which matches the bitness of the PowerShell runtime will be used (e.g. 64-bit VSTOInstaller.exe with 64-bit PowerShell). 30 | 31 | Parameters for VSTOInstaller.exe 32 | https://learn.microsoft.com/en-us/previous-versions/visualstudio/visual-studio-2010/bb772078(v=vs.100)#parameters-for-vstoinstallerexe 33 | 34 | VSTOInstaller Error Codes 35 | https://learn.microsoft.com/en-us/previous-versions/visualstudio/visual-studio-2010/bb757423(v=vs.100) 36 | 37 | .LINK 38 | https://github.com/ralish/PSWinGlue 39 | #> 40 | 41 | #Requires -Version 3.0 42 | 43 | [CmdletBinding()] 44 | [OutputType([Void])] 45 | Param( 46 | [Parameter(Mandatory)] 47 | [String]$ManifestPath, 48 | 49 | [Switch]$Silent 50 | ) 51 | 52 | $PowerShellCore = New-Object -TypeName Version -ArgumentList 6, 0 53 | if ($PSVersionTable.PSVersion -ge $PowerShellCore -and $PSVersionTable.Platform -ne 'Win32NT') { 54 | throw '{0} is only compatible with Windows.' -f $MyInvocation.MyCommand.Name 55 | } 56 | 57 | $VstoInstaller = '{0}\Microsoft Shared\VSTO\10.0\VSTOInstaller.exe' -f $env:CommonProgramFiles 58 | $VstoArguments = @('/Uninstall', ('"{0}"' -f $ManifestPath)) 59 | 60 | if ($Silent) { 61 | $VstoArguments += '/Silent' 62 | } 63 | 64 | if (!(Test-Path -Path $VstoInstaller)) { 65 | throw 'VSTO Installer not present at: {0}' -f $VstoInstaller 66 | } 67 | 68 | Write-Verbose -Message ('Uninstalling VSTO add-in with arguments: {0}', [String]::Join(' ', $VstoArguments)) 69 | $VstoProcess = Start-Process -FilePath $VstoInstaller -ArgumentList $VstoArguments -Wait -PassThru 70 | if ($VstoProcess.ExitCode -ne 0) { 71 | throw 'VSTO Installer failed with code: {0}' -f $VstoProcess.ExitCode 72 | } 73 | -------------------------------------------------------------------------------- /Scripts/Update-GitRepository.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Updates a Git repository 4 | 5 | .DESCRIPTION 6 | A helper function to automate updating a Git repository. 7 | 8 | The following steps are performed with a failure aborting subsequent steps: 9 | - Check the current directory is a valid Git repository 10 | - Add all changes in the working tree to the staging area 11 | - If there are staged changes commit them to the repository 12 | - Pull changes from upstream and perform a fast-forward merge 13 | - Push the new commit(s) to the upstream repository 14 | 15 | .EXAMPLE 16 | Update-GitRepository 17 | 18 | Updates the Git repository in the current directory. See the description section for the steps performed. 19 | 20 | .NOTES 21 | This function is primarily intended for usage in unattended scenarios, such as invoking from a Scheduled Task. 22 | 23 | The following potential issues are handled, which are more common with or specific to Scheduled Task execution: 24 | - Checks Git is installed, and if so, retrieves its installation directory 25 | This enables the function to handle the case where the Git binaries aren't in the PATH of the executing user. 26 | - Ensures the environment is initialised for Git to function correctly 27 | In particular, the HOME environment variable may be missing when run as a Scheduled Task, causing SSH issues. 28 | 29 | .LINK 30 | https://github.com/ralish/PSWinGlue 31 | #> 32 | 33 | #Requires -Version 3.0 34 | 35 | [CmdletBinding()] 36 | [OutputType([String[]])] 37 | Param() 38 | 39 | $PowerShellCore = New-Object -TypeName Version -ArgumentList 6, 0 40 | if ($PSVersionTable.PSVersion -ge $PowerShellCore -and $PSVersionTable.Platform -ne 'Win32NT') { 41 | throw '{0} is only compatible with Windows.' -f $MyInvocation.MyCommand.Name 42 | } 43 | 44 | Function Initialize-GitEnvironment { 45 | [CmdletBinding()] 46 | [OutputType([Void])] 47 | Param( 48 | [Parameter(Mandatory)] 49 | [String]$Path 50 | ) 51 | 52 | # Amend the PATH variable to include the full set of Git utilities 53 | $Env:Path = '{0}\bin;{1}' -f $Path, $Env:Path 54 | 55 | # Create the HOME environment variable needed by SSH (via Git) 56 | # 57 | # When running as a Scheduled Task the user profile of the account may not 58 | # yet be loaded on Windows 8 or Server 2012 and newer. This can cause the 59 | # USERPROFILE environment variable to point to the Default user profile. A 60 | # workaround is to retrieve and set the correct path using GetFolderPath(). 61 | # 62 | # Scheduled tasks reference incorrect user profile paths 63 | # https://support.microsoft.com/en-us/kb/2968540 64 | $Env:HOME = [Environment]::GetFolderPath([Environment+SpecialFolder]::UserProfile) 65 | } 66 | 67 | Function Test-GitInstalled { 68 | [CmdletBinding()] 69 | [OutputType([String])] 70 | Param() 71 | 72 | # Registry keys potentially containing Git installation details 73 | $GitRegistryKeys = @( 74 | # User 75 | 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Git_is1', 76 | # Machine: Native bitness 77 | 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Git_is1', 78 | # Machine: x86 on x64 79 | 'HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\Git_is1' 80 | ) 81 | 82 | # Registry property which contains the installation directory 83 | $GitInstallProperty = 'InstallLocation' 84 | 85 | foreach ($RegKey in $GitRegistryKeys) { 86 | try { 87 | $RegKeyProps = Get-ItemProperty -Path $RegKey -ErrorAction Stop 88 | $GitInstallPath = $RegKeyProps.$GitInstallProperty 89 | } catch { 90 | continue 91 | } 92 | 93 | if ([String]::IsNullOrWhiteSpace($GitInstallPath)) { 94 | continue 95 | } 96 | 97 | if (!(Test-Path -Path $GitInstallPath -PathType Container)) { 98 | continue 99 | } 100 | 101 | return $GitInstallPath 102 | } 103 | 104 | throw 'Unable to locate a Git installation.' 105 | } 106 | 107 | Function Test-GitRepository { 108 | [CmdletBinding()] 109 | [OutputType([Void])] 110 | Param() 111 | 112 | $null = & git rev-parse --git-dir 2>&1 113 | if ($LASTEXITCODE -ne 0) { 114 | throw 'The current directory does not belong to a Git repository: {0}' -f $PWD.Path 115 | } 116 | } 117 | 118 | Function Update-GitRepository { 119 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] 120 | [CmdletBinding()] 121 | [OutputType([String[]])] 122 | Param() 123 | 124 | & git add --all 125 | if ($LASTEXITCODE -ne 0) { 126 | throw 'Something went wrong adding all changes to the Git index.' 127 | } 128 | 129 | # Check if the index is dirty indicating we have something to commit 130 | & git diff-index --quiet --cached HEAD 131 | if ($LASTEXITCODE -ne 0) { 132 | $GitCommitDate = Get-Date -UFormat '%d/%m/%Y' 133 | git commit -m ('Changes for {0}' -f $GitCommitDate) 134 | if ($LASTEXITCODE -ne 0) { 135 | throw 'Something went wrong committing all changes in the Git index.' 136 | } 137 | } 138 | 139 | & git pull 140 | if ($LASTEXITCODE -ne 0) { 141 | throw 'Something went wrong pulling from the remote Git repository.' 142 | } 143 | 144 | & git push 145 | if ($LASTEXITCODE -ne 0) { 146 | throw 'Something went wrong pushing to the remote Git repository.' 147 | } 148 | } 149 | 150 | # Test Git software is installed 151 | $GitInstallPath = Test-GitInstalled 152 | 153 | # Check we're in a Git repository 154 | Test-GitRepository 155 | 156 | # Initialize the Git environment 157 | Initialize-GitEnvironment -Path $GitInstallPath 158 | 159 | # Commit all and push all changes 160 | Update-GitRepository 161 | -------------------------------------------------------------------------------- /Scripts/Update-OneDriveSetup.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Update OneDrive during Windows image creation 4 | 5 | .DESCRIPTION 6 | The version of OneDrive bundled with a Windows release will often be out-of-date by the time of deployment to a client system. 7 | 8 | While OneDrive will update itself, updates for older versions can require Administrator privileges, which users may not have. 9 | 10 | By pre-installing the latest update, unwanted UAC prompts can be avoided, while also streamlining the initial logon process. 11 | 12 | .PARAMETER SetupDestDir 13 | Path to the directory where the OneDrive setup file will be stored. 14 | 15 | The default is "$env:ProgramData\Microsoft\OneDriveSetup". 16 | 17 | .PARAMETER SetupFilePath 18 | Path to the OneDrive setup file. 19 | 20 | The default is "OneDriveSetup.exe" in the current working directory. 21 | 22 | .PARAMETER SkipRunningSetup 23 | Skips executing OneDrive setup. 24 | 25 | .PARAMETER SkipUpdatingDefaultProfile 26 | Skips updating the default user profile to run OneDrive setup on login. 27 | 28 | .EXAMPLE 29 | Update-OneDriveSetup 30 | 31 | Copies OneDrive setup to the default destination path, runs setup, and updates the default user profile. 32 | 33 | .NOTES 34 | OneDrive release notes 35 | https://support.microsoft.com/en-us/office/onedrive-release-notes-845dcf18-f921-435e-bf28-4e24b95e5fc0 36 | 37 | .LINK 38 | https://github.com/ralish/PSWinGlue 39 | #> 40 | 41 | #Requires -Version 3.0 42 | 43 | [CmdletBinding()] 44 | [OutputType([Void])] 45 | Param( 46 | [ValidateNotNullOrEmpty()] 47 | [String]$SetupFilePath = 'OneDriveSetup.exe', 48 | 49 | [ValidateNotNullOrEmpty()] 50 | [String]$SetupDestDir = "$env:ProgramData\Microsoft\OneDriveSetup", 51 | 52 | [Switch]$SkipRunningSetup, 53 | [Switch]$SkipUpdatingDefaultProfile 54 | ) 55 | 56 | $PowerShellCore = New-Object -TypeName Version -ArgumentList 6, 0 57 | if ($PSVersionTable.PSVersion -ge $PowerShellCore -and $PSVersionTable.Platform -ne 'Win32NT') { 58 | throw '{0} is only compatible with Windows.' -f $MyInvocation.MyCommand.Name 59 | } 60 | 61 | $User = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent() 62 | if (!$User.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { 63 | throw '{0} requires Administrator privileges.' -f $MyInvocation.MyCommand.Name 64 | } 65 | 66 | if ($SkipRunningSetup -and $SkipUpdatingDefaultProfile) { 67 | throw 'Nothing to do as all operations skipped!' 68 | } 69 | 70 | $SetupFile = Get-Item -Path $SetupFilePath -ErrorAction Stop 71 | if ($SetupFile -isnot [IO.FileInfo]) { 72 | throw 'OneDrive setup path is not a file: {0}' -f $SetupFilePath 73 | } elseif ($SetupFile.Extension -ne '.exe') { 74 | throw 'OneDrive setup file must be an executable: {0}' -f $SetupFilePath 75 | } 76 | 77 | if (!(Test-Path -Path $SetupDestDir -PathType Container)) { 78 | Write-Verbose -Message 'Creating OneDrive setup directory ...' 79 | $null = New-Item -Path $SetupDestDir -ItemType Directory -Force -ErrorAction Ignore 80 | } 81 | 82 | $SetupDest = Get-Item -Path $SetupDestDir -ErrorAction Stop 83 | if ($SetupDest -isnot [IO.DirectoryInfo]) { 84 | throw 'OneDrive setup destination is not a directory: {0}' -f $SetupDestDir 85 | } 86 | 87 | Write-Verbose -Message 'Copying OneDrive setup file ...' 88 | $SetupDestFilePath = Join-Path -Path $SetupDestDir -ChildPath 'OneDriveSetup.exe' 89 | Copy-Item -Path $SetupFilePath -Destination $SetupDestFilePath -Force 90 | [IO.FileInfo]$SetupFilePath = Get-Item -Path $SetupDestFilePath -ErrorAction Stop 91 | 92 | if (!$SkipRunningSetup) { 93 | Write-Verbose -Message 'Terminating any existing OneDrive setup ...' 94 | Stop-Process -Name 'OneDriveSetup' -ErrorAction Ignore 95 | 96 | Write-Verbose -Message 'Running OneDrive setup ...' 97 | $OneDriveSetup = Start-Process -FilePath $SetupFilePath.FullName -ArgumentList '/silent' -Wait -PassThru 98 | if ($OneDriveSetup.ExitCode -notin ('0', '3010')) { 99 | throw ('OneDrive setup returned exit code: {0}' -f $OneDriveSetup.ExitCode) 100 | } 101 | } 102 | 103 | if (!$SkipUpdatingDefaultProfile) { 104 | Write-Verbose -Message 'Updating OneDrive setup path for default user profile ...' 105 | 106 | if (!('PSWinGlue.UpdateOneDriveSetup' -as [Type])) { 107 | $GetDefaultUserProfileDirectory = @' 108 | [DllImport("userenv.dll", EntryPoint = "GetDefaultUserProfileDirectoryW", SetLastError = true)] 109 | public static extern bool GetDefaultUserProfileDirectory(IntPtr lpProfileDir, out uint lpcchSize); 110 | 111 | [DllImport("userenv.dll", CharSet = CharSet.Unicode, EntryPoint = "GetDefaultUserProfileDirectoryW", ExactSpelling = true, SetLastError = true)] 112 | public static extern bool GetDefaultUserProfileDirectory(System.Text.StringBuilder lpProfileDir, out uint lpcchSize); 113 | '@ 114 | 115 | Add-Type -Namespace 'PSWinGlue' -Name 'UpdateOneDriveSetup' -MemberDefinition $GetDefaultUserProfileDirectory 116 | } 117 | 118 | $DefaultUserProfileBufSize = 0 119 | $null = [PSWinGlue.UpdateOneDriveSetup]::GetDefaultUserProfileDirectory([IntPtr]::Zero, [ref]$DefaultUserProfileBufSize) 120 | if (!$DefaultUserProfileBufSize) { 121 | throw 'Failed to determine buffer size for GetDefaultUserProfileDirectory().' 122 | } 123 | 124 | $DefaultUserProfilePath = New-Object -TypeName 'Text.StringBuilder' -ArgumentList ($DefaultUserProfileBufSize - 1) 125 | if ([PSWinGlue.UpdateOneDriveSetup]::GetDefaultUserProfileDirectory($DefaultUserProfilePath, [ref]$DefaultUserProfileBufSize) -eq $false) { 126 | throw (New-Object -TypeName 'ComponentModel.Win32Exception') 127 | } 128 | 129 | $DefaultUserProfileHive = Join-Path -Path $DefaultUserProfilePath.ToString() -ChildPath 'NTUSER.DAT' 130 | Write-Debug -Message ('Default user profile registry hive: {0}' -f $DefaultUserProfileHive) 131 | 132 | $Registry = Start-Process -FilePath 'reg' -ArgumentList 'LOAD', 'HKLM\OneDriveSetup', "`"$DefaultUserProfileHive`"" -Wait -PassThru 133 | if ($Registry.ExitCode -ne 0) { 134 | throw 'Failed to load the default user profile registry hive with error code: {0}' -f $Registry.ExitCode 135 | } 136 | 137 | Set-ItemProperty -Path 'HKLM:\OneDriveSetup\Software\Microsoft\Windows\CurrentVersion\Run' -Name 'OneDriveSetup' -Value $SetupFilePath.FullName 138 | 139 | $Registry = Start-Process -FilePath 'reg' -ArgumentList 'UNLOAD', 'HKLM\OneDriveSetup' -Wait -PassThru 140 | if ($Registry.ExitCode -ne 0) { 141 | throw 'Failed to unload the default user profile registry hive with error code: {0}' -f $Registry.ExitCode 142 | } 143 | } 144 | --------------------------------------------------------------------------------