TM-WindowsUtility.psm1
using namespace System.Collections.Generic using namespace System.IO # Write verbose messages on import if ((Get-PSCallStack)[1].Arguments -imatch 'Verbose=True') { $PSDefaultParameterValues['*:Verbose'] = $true } if ($env:OS -ne 'Windows_NT') { Write-Verbose 'TM-WindowsUtility should only be run on windows.' return } Set-Alias -Name 'which' -Value 'where.exe' function Get-COMDetails { <# .SYNOPSIS Gets extended COM file properties. .DESCRIPTION This function retrieves extended file properties using COM (Component Object Model) and returns the Key/Value pairs in a PSCustomObject. .PARAMETER File The FileInfo object that has extended properties to analyze. #> [CmdletBinding()] [OutputType([PSCustomObject])] param ( [Parameter(Mandatory, ValueFromPipeline)] [Alias('Path', 'PSPath')] [Validation.ValidatePathExists('File')] [FileInfo]$File ) begin { $Output = [PSCustomObject]@{ FullName = $File.FullName } } process { $objShell = New-Object -ComObject Shell.Application $objFolder = $objShell.Namespace($File.DirectoryName) $objFile = $objFolder.ParseName($File.Name) if ($objFile) { for ($ColumnNumber = 0; $ColumnNumber -le 1000; $ColumnNumber++) { $ColumnName = $null $ColumnValue = $null $ColumnName = $objFolder.GetDetailsOf($null, $ColumnNumber) $ColumnValue = $objFolder.GetDetailsOf($ObjFile, $ColumnNumber) if ($ColumnValue) { # Ensure we always have a property name. if (-Not $ColumnName) { $ColumnName = "ColumnNumber$ColumnNumber" } $Output.PSObject.Properties.Add( [PSNoteProperty]::New($ColumnName, $ColumnValue) ) } } } } end { return $Output } } function Get-DNSNamesBySubnet { <# .SYNOPSIS Attempts to retrieve DNS names for devices active within in a specified subnet. .DESCRIPTION This function queries DNS names for devices in a specified subnet and returns a list of objects containing their IPAddress, DeviceName, and ping statuses. .PARAMETER Subnet The subnet to query for DNS names. Should be a valid IP address. .PARAMETER CIDR The CIDR value for the subnet (default is 24). Acceptable values are between 24 and 30. .PARAMETER PassThru A switch that indicates that the output should be returned as a list of objects instead of a formatted table. .PARAMETER IncludeAllResults A switch that includes all results, including for IPs with empty DNS names. #> [CmdletBinding()] [OutputType([void], [PSCustomObject])] param ( [Parameter( Position = 0, Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Validation.ValidateIPv4Format()] [string]$IPV4Address, [Parameter( Position = 1, Mandatory = $false, ValueFromPipeline )] [ValidateRange(24, 30)] [int]$CIDR = 24, [Parameter(Mandatory = $false)] [switch]$PassThru, [Parameter(Mandatory = $false)] [switch]$IncludeAllResults ) begin { if ((Test-ApplicationExistsInPath -ApplicationName 'nslookup') -eq $false) { throw 'Get-DNSNamesBySubnet requires nslookup to run but it is not available in the PATH.' } $FirstThreeOctets = $IPV4Address -replace "$($IPV4Address.Split('.')[-1])$", '' [int]$FinalOctet = $IPV4Address -replace "^$FirstThreeOctets", '' $Output = [List[object]]::New() $CIDRAdressCount = switch ($CIDR) { 24 { 254 } 25 { 126 } 26 { 62 } 27 { 30 } 28 { 14 } 29 { 7 } 30 { 2 } } $End = $FinalOctet + $CIDRAdressCount if ($End -gt 255) { $End = 255 } } process { $JobOutput = $FinalOctet..$End | ForEach-Object -Parallel { try { $Address = $stdout = $DNSName = $null $Address = "$using:FirstThreeOctets$($using:FinalOctet + $_)" $null = . { nslookup $Address | Set-Variable stdout } 2>&1 | ForEach-Object ToString try { $selectStringSplat = @{ InputObject = $stdout Pattern = 'Name:' SimpleMatch = $true ErrorAction = 'Stop' } $DNSName = (Select-String @selectStringSplat)[0].line.split(':')[-1].trim() } catch { } if ($using:IncludeAllResults -or (-Not [string]::IsNullOrWhiteSpace($DNSName))) { return [PSCustomObject]@{ FinalOctet = ($using:FinalOctet + $_) IPAddress = $Address DeviceName = $DNSName Connection = (Test-Connection $Address) } } } catch { } } $JobOutput = $JobOutput | Sort-Object -Property FinalOctet foreach ($Job in $JobOutput) { $Output.Add( [PSCustomObject]@{ IPAddress = $Job.IPAddress DeviceName = $Job.DeviceName Ping01 = $Job.Connection[0].status Ping02 = $Job.Connection[1].status Ping03 = $Job.Connection[2].status Ping04 = $Job.Connection[3].status } ) } } end { if ($Output.Length -eq 0) { Write-Host 'No DNS names found.' } if ($PassThru) { return $Output } else { ($Output | Format-Table -Property *) } } } function New-ConsoleArgWriter { <# .SYNOPSIS Creates a new 'ConsoleArgWriter' console application if it does not already exist. .DESCRIPTION The New-ConsoleArgWriter function generates an executable, ConsoleArgWriter.exe, in the specified $ShellBinPath. Use this executable as a mock for other executables when testing. ConsoleArgWriter.exe will write the input arguments to the console, rather than executing the actual application. This is useful for testing and debugging scripts that utilize command-line applications when you don't actually want to run the executables. This function checks if the 'ConsoleArgWriter.exe' already exists in the specified path. If it doesn't, it uses the Add-Type cmdlet to compile a new console application from a C# script. The new application simply writes the provided arguments to the console. To use the executable after running this function, alias the desired executable name to ConsoleArgWriter.exe. For example: Set-Alias -Name 'gpg.exe' -Value (Join-Path $ShellPath 'bin' 'ConsoleArgWriter.exe') & 'gpg.exe' --pinentry-mode=loopback "--d=$true" In this case, any arguments passed to 'gpg.exe' would be written to the console instead of being passed to the actual gpg.exe executable. .PARAMETER ShellBinPath The path where the ConsoleArgWriter.exe will be created. .EXAMPLE New-ConsoleArgWriter -ShellBinPath C:\temp This will create a new ConsoleArgWriter.exe in the C:\temp directory. .NOTES The New-ConsoleArgWriter function should be used for debugging and testing purposes. Avoid using it in production environments as it could potentially expose sensitive command-line arguments. #> [CmdletBinding()] param ( [Parameter(Mandatory)] [Validation.ValidatePathExists('Folder')] [string]$ShellBinPath ) $ArgWriter = Join-Path -Path $ShellBinPath -ChildPath 'ConsoleArgWriter.exe' if ((Test-Path -Path $ArgWriter) -eq $false) { if ($env:Path.Split([System.IO.Path]::PathSeparator) -notcontains $ShellBinPath) { Write-Warning ( "The ShellBinPath '$ShellBinPath' is not in your `$Env:Path. To use the ConsoleArgWriter you will need " + "to alias the entire path '$ArgWriter'." ) } # Starting this in Windows PowerShell because it can emit console applications. $CreateConsoleWriter = ([hashtable]@{ FilePath = 'powershell.exe' NoNewWindow = $true WorkingDirectory = $ShellBinPath ArgumentList = ( '-noprofile', '-ec', [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes(@' [hashtable]$ConsoleWriterSplat = @{ OutputType = 'ConsoleApplication' Language = 'CSharp' OutputAssembly = 'ConsoleArgWriter.exe' TypeDefinition = @" using System; public class Program { public static int Main(string[] args) { int count = 0; foreach (string arg in args) { if (null == arg) { Console.WriteLine("{0}: null", count); } else { Console.WriteLine("{0}: \"{1}\"", count, arg); } count++; } return 0; } } "@ } Add-Type @ConsoleWriterSplat '@))) }) Start-Process @CreateConsoleWriter } } function touch { <# .SYNOPSIS Create a new file at the specified path. .DESCRIPTION The touch function emulates the Unix touch command and creates a new file at the specified path if it does not already exist. .PARAMETER Path The path where the new file will be created. #> [CmdletBinding()] [OutputType([Void])] param ( [Parameter( Mandatory, ValueFromPipeline, Position = 0 )] [string]$Path ) process { New-Item -ItemType File -Path $Path } } function tail { <# .SYNOPSIS Display the last 'n' lines of a file. .DESCRIPTION The tail function emulates the Unix tail command and displays the last 'n' lines of a file. .PARAMETER Path The path of the file to display. .PARAMETER LiteralPath The literal path of the file to display. .PARAMETER follow A switch that indicates that the function should follow the file as it grows. .PARAMETER lines The number of lines to display from the end of the file. Default is 10. .EXAMPLE # Use Control C to exit the function and stop tailing the file. tail -f 'C:\Path\To\File.txt' #> [CmdletBinding()] [OutputType([Void])] param ( [Parameter( Mandatory, ValueFromPipeline, ParameterSetName = 'Path', Position = 0 )] [Validation.ValidatePathExists('File')] [FileInfo]$Path, [Parameter( Mandatory, ValueFromPipeline, ParameterSetName = 'LiteralPath', Position = 0 )] [Validation.ValidatePathExists('File')] [string]$LiteralPath, [Parameter(Mandatory = $false)] [switch]$follow, [Parameter(Mandatory = $false)] [Alias('n')] [int]$lines = 10 ) $ParameterSplat = @{} if ($Path) { $ParameterSplat.Add('Path', $Path) } if ($LiteralPath) { $ParameterSplat.Add('LiteralPath', $LiteralPath) } if ($follow) { $ParameterSplat.Add('Wait', $true) } $ParameterSplat.Add('Tail', $lines) Get-Content @ParameterSplat } function Set-WindowFocus { <# .SYNOPSIS Sets the focus on a specified window. .DESCRIPTION This function shifts focus to a specified window by utilizing either the process name or the process object. By invoking Windows APIs, the function manages the visibility to show or hide GUI process windows. .PARAMETER ProcessName The name of the process whose window you want to focus. .PARAMETER Process The process object whose window you want to focus. .PARAMETER Minimize A switch that indicates if the window should be minimized after setting the focus. #> [CmdletBinding()] [OutputType([Void])] param ( [Parameter( Mandatory, ValueFromPipeline, ParameterSetName = 'ProcessName', Position = 0 )] [string]$ProcessName, [Parameter( Mandatory, ValueFromPipeline, ParameterSetName = 'Process', Position = 0 )] [ComponentModel.Component]$Process, [Parameter(Mandatory = $false)] [switch]$Minimize ) # https://stackoverflow.com/a/58548853 Add-Type -Namespace ProfileUtility -Name WindowVisibility -MemberDefinition @' [DllImport("user32.dll", SetLastError=true)] public static extern bool SetForegroundWindow(IntPtr hWnd); [DllImport("user32.dll", SetLastError=true)] public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); [DllImport("user32.dll", SetLastError=true)] public static extern bool IsIconic(IntPtr hWnd); '@ switch ($PSCmdlet.ParameterSetName) { 'ProcessName' { $ProcessName = $ProcessName -replace '\.exe$' $Processes = (Get-Process -ErrorAction Ignore $ProcessName) if ($Processes.Count -gt 1) { $Index = 0 $ProcessList = Foreach ($Process in $Processes) { [PSCustomObject]@{ Number = $Index WindowTitle = $Process.MainWindowTitle } ++$Index } $PromptAnswer = Read-Host -Prompt ( "Found multiple processes for '$ProcessName'. Which would you like to select.`n" + ($ProcessList | Format-Table | Out-String) ) $hWnd = $Processes[$PromptAnswer].MainWindowHandle } else { try { $hWnd = $Processes[0].MainWindowHandle } catch { <# Do Nothing #> } } } 'Process' { $hWnd = $Process.MainWindowHandle } } if (-not $hWnd) { Throw 'Failed to retrieve MainWindowHandle.' } # https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-showwindow if ($Minimize) { [ProfileUtility.WindowVisibility]::ShowWindow($hwnd, 6) | Out-Null } else { # Set the focus on the window. [ProfileUtility.WindowVisibility]::SetForegroundWindow($hWnd) | Out-Null # If the window is minimized, restore it. if ([ProfileUtility.WindowVisibility]::IsIconic($hwnd)) { [ProfileUtility.WindowVisibility]::ShowWindow($hwnd, 9) | Out-Null } } } function Wait-Active { <# .SYNOPSIS Keeps the computer active for a specified amount of time. .DESCRIPTION This function opens Notepad and sends a key press to keep the computer active. The duration can be specified in minutes, as a TimeSpan, or as a DateTime. .PARAMETER StopTime The DateTime at which the function should stop keeping the computer active. .PARAMETER Duration The TimeSpan duration to keep the computer active. .PARAMETER Minutes The number of minutes to keep the computer active. .PARAMETER SleepSeconds The number of seconds to sleep between each activity (default is 60 seconds). #> [CmdletBinding(DefaultParameterSetName = 'Minutes')] [OutputType([Void])] param ( [Parameter( Position = 0, ParameterSetName = 'DateTime', Mandatory = $false, ValueFromPipeline, ValueFromPipelineByPropertyName )] [DateTime]$StopTime = (Get-Date).AddMinutes(5), [Parameter( Position = 0, ParameterSetName = 'TimeSpan', Mandatory = $false, ValueFromPipeline, ValueFromPipelineByPropertyName )] [ValidateRange(0, [int]::MaxValue)] [TimeSpan]$Duration = (New-TimeSpan -Start (Get-Date) -End (Get-Date).AddMinutes(5)), [Parameter( Position = 0, ParameterSetName = 'Minutes', Mandatory = $false, ValueFromPipeline, ValueFromPipelineByPropertyName )] [ValidateRange(0, [int]::MaxValue)] [int]$Minutes = 5, [Parameter( Position = 1, Mandatory = $false, ValueFromPipelineByPropertyName )] [ValidateRange(0, [int]::MaxValue)] [Int]$SleepSeconds = 60 ) $WaitTill = switch ($PSCmdlet.ParameterSetName) { 'DateTime' { $StopTime } 'TimeSpan' { (Get-Date).AddSeconds($timespan.TotalSeconds) } 'Minutes' { (Get-Date).AddMinutes($Minutes) } } $Process = Start-Process 'Notepad.exe' -PassThru Start-Sleep -Seconds 5 do { if ((Get-Process -InputObject $Process).HasExited) { Write-Warning 'Notepad has exited.' return } Set-WindowFocus $Process (New-Object -ComObject wscript.shell).SendKeys('.') Set-WindowFocus $Process -Minimize Start-Sleep -Seconds $SleepSeconds $CurrentTime = Get-Date } while ($CurrentTime -lt $WaitTill) Stop-Process $Process } |