ds-utils.psm1

#requires -RunAsAdministrator

<#
.SYNOPSIS
    Return true if a reboot is pending (local machine)
.EXAMPLE
    if (Test-RebootPending) { ... }
.NOTES
    Thanks to https://4sysops.com/archives/use-powershell-to-test-if-a-windows-server-is-pending-a-reboot/
.OUTPUTS
    True or False
#>

function Test-RebootPending {
    [CmdletBinding()]
    param ()
    $pendingRebootTests = @(
        @{
            Name = 'RebootPending'
            Test = { Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing' -Name 'RebootPending' -ErrorAction Ignore }
            TestType = 'ValueExists'
        }
        @{
            Name = 'RebootRequired'
            Test = { Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update' -Name 'RebootRequired' -ErrorAction Ignore }
            TestType = 'ValueExists'
        }
        @{
            Name = 'PendingFileRenameOperations'
            Test = { Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager' -Name 'PendingFileRenameOperations' -ErrorAction Ignore }
            TestType = 'NonNullValue'
        }
    )
    foreach ($test in $pendingRebootTests) {
        $result = Invoke-Command -ScriptBlock $test.Test
        if ($test.TestType -eq 'ValueExists' -and $result) {
            $true
        }
        elseif ($test.TestType -eq 'NonNullValue' -and $result -and $result.($test.Name)) {
            $true
        }
        else {
            $false
        }
    }
}

<#
.SYNOPSIS
.DESCRIPTION
.PARAMETER LogFile
.PARAMETER Category
.PARAMETER Message
#>

function Write-DsLog {
    [CmdletBinding()]
    param (
        [parameter()][ValidateNotNullOrEmpty()] [string] $LogFile = $(Join-Path $env:SystemRoot "temp\ds-utils-$(Get-Date -f 'yyyyMMddhhmm').log"),
        [parameter()][ValidateSet('Info','Error','Warning')] [string] $Category = 'Info',
        [parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $Message
    )
    try {
        $strdata = "$(Get-Date -f 'yyyy-MM-dd hh:mm:ss') - $Category - $Message"
        $strdata | Out-File -FilePath $LogFile -Append
        switch ($Category) {
            'Warning' { Write-Warning $strdata }
            'Error' { Write-Warning $strdata }
            default { Write-Host $strdata -ForegroundColor Cyan }
        }
    }
    catch {
        Write-Error "Write-DsLog (error): $($Error[0].Exception.Message)"
    }
}

<#
.SYNOPSIS
.DESCRIPTION
.PARAMETER Update
.EXAMPLE
.NOTES
.OUTPUTS
#>

function Invoke-DsMaintenance {
    [CmdletBinding()]
    param (
        [parameter()] [ValidateSet('All','Modules','Windows','Packages')] [string] $Update = 'All',
        [parameter()] [switch] $ForceReboot
    )
    try {
        switch ($Update) {
            'All' {
                Write-DsLog -Message "updating powershell modules"
                Update-Module
                Write-DsLog -Message "powershell modules have been updated"
                if (Test-Path (Join-Path $env:ProgramData "chocolatey\choco.exe")) {
                    Write-DsLog -Message "updating chocolatey packages"
                    cup all -y
                    Write-DsLog -Message "chocolatey packages have been updated"
                }
                else {
                    Write-DsLog -Message  "chocolatey is not installed (skipping updates)" -Category 'Warning'
                    Write-Warning "chocolatey is not installed (skipping package updates)"
                }
                Write-DsLog "updating windows and office products"
                $res = Get-WindowsUpdate -AcceptAll -Install -WindowsUpdate -IgnoreReboot
                Write-DsLog "$($res.Count) windows updates were applied"
            }
        }
        if (Test-RebootPending) {
            Write-DsLog "tasks completed (reboot required)"
            if ($ForceReboot) {
                Write-Output 1641
                Write-DsLog "rebooting computer in 15 seconds"
                Restart-Computer -Timeout 15
            }
        }
        else {
            Write-DsLog "tasks completed"
            Write-Output 0
        }
    }
    catch {
        Write-DsLog -Category 'Error' -Message "$($Error[0].Exception.Message)"
        Write-Output -1
    }
}

<#
.SYNOPSIS
    Rename computer using common standard format
.DESCRIPTION
    I hate repeating myself
.PARAMETER MaxNameLength
    Maximum length of new name (default is 15, which is the limit for Windows)
.PARAMETER FormCode
    Form-factor code placement: Prefix (default), Suffix, or None
.PARAMETER NoHyphen
    Do not insert a hyphen separator between FormCode and SerialNumber
.PARAMETER Reboot
    Force a reboot at the end (default = no reboot)
.EXAMPLE
    Set-DsComputerName
    (Defaults) results in name like "L-123456789"
.EXAMPLE
    Set-DsComputerName -FormCode Suffix -NoHyphen
    Results in name like "123456789L"
.EXAMPLE
    Set-DsComputerName -FormCode None -MaxNameLength 8
    Results in name like "12345678"
.NOTES
    Actual Serial Number is used from WMI class Win32_SystemEnclosure
    Chassis Type number is taken from Win32_SystemEnclosure and uses first
        element of result only, since docking stations, port replicators
        may return an array like (10,12) where 10 is the laptop, and 12 is the dock
#>

function Set-DsComputerName {
    [CmdletBinding(SupportsShouldProcess)]
    param (
        [parameter()][ValidateRange(3,15)][int] $MaxNameLength = 15,
        [parameter()][ValidateSet('Prefix','Suffix','None')][string] $FormCode = 'Prefix',
        [parameter()][switch] $NoHyphen,
        [parameter()][switch] $Reboot
    )
    # rename computer to "X-12345678"
    [string]$sn = (Get-WmiObject -Class Win32_SystemEnclosure).SerialNumber
    [int]$ct = ((Get-WmiObject -Class Win32_SystemEnclosure).ChassisTypes)[0]
    Write-Verbose "serialnumber = $sn"
    Write-Verbose "chassistype = $ct"
    # desktops
    if ($ct -in (3..7)+(13,34,35)) { $ff = 'D' }
    # laptops
    elseif ($ct -in (10,11,12,14)+(15..30)+(31,32,33,36)) { $ff = 'L' }
    # servers
    elseif ($ct -in (17..24)) { $ff = 'S' }
    # unknown
    else { $ff = 'X' }
    if ($NoHyphen) { $sep = "" } else { $sep = "-" }
    if ($FormCode -eq 'None') { $fc = ""; $sep = "" } else { $fc = $ff }
    $nx = "$fc$sep$sn"
    if ($nx.Length -gt $MaxNameLength) {
        $over = $nx.Length - $MaxNameLength
        $sn = $sn.substring($over, $sn.Length - $over)
        $nx = "$fc$sep$sn"
    }
    Write-Host "renaming computer to $nx" -ForegroundColor cyan
    if ($Reboot) {
        Rename-Computer -NewName $nx -Force -Restart
    }
    else {
        Rename-Computer -NewName $nx -Force
    }
}

<#
#>


function Install-DsPackages {
    [CmdletBinding()]
    param (
        [parameter()]
        [ValidateNotNullOrEmpty()]
        [string[]] $Packages = ('dotnet3.5','7zip','notepadplusplus','adobereader','googlechrome')
    )
    try {
        if (!(Test-Path (Join-Path $env:ProgramData "Chocolatey\choco.exe"))) {
            Write-Host "installing chocolatey" -ForegroundColor cyan
            Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
            Write-Verbose "chocolatey has landed!"
        }
        cup $Packages -y
        Write-Output 0
    }
    catch {
        Write-Error $Error[0].Exception.Message
        Write-Output -1
    }
}

function Get-DsPowerPlan {
    param()
    $pplist = POWERCFG -LIST | Where-Object {$_ -like "Power Scheme*"}
    foreach ($pp in $pplist) {
        $pdata = (($pp.Split(":")).Trim())[1]
        $pguid = $pdata.Split(" ")[0]
        $pname = ($pdata.Split("(")[1]).Replace(")","")
        if ($pname.EndsWith("`*")) {
            $pname = $pname.Replace(" `*","")
            $data = @{
                Name = $pname
                GUID = $pguid
                IsActive = $True
            }
        }
        else {
            $data = @{
                Name = $pname
                GUID = $pguid
                IsActive = $False
            }
        }
        $xdata = New-Object -TypeName PSObject -Property $data
        Write-Output $xdata
    }
}

function Set-DsPowerPlan {
    [CmdletBinding()]
    param (
        [parameter(Mandatory=$True, HelpMessage="Power scheme name")]
            [ValidateSet('Balanced','HighPerformance','Performance','PowerSaver','EnergyStar','Custom')]
            [string] $PlanName,
        [parameter(Mandatory=$False, HelpMessage="Custom power plan filename")]
            [ValidateNotNullOrEmpty()]
            [string] $FileName = ""
    )
    #Power Scheme GUID: 1ca6081e-7f76-46f8-b8e5-92a6bd9800cd (Maximum Battery
    #Power Scheme GUID: 2ae0e187-676e-4db0-a121-3b7ddeb3c420 (Power Source Opt
    #Power Scheme GUID: 37aa8291-02f6-4f6c-a377-6047bba97761 (Timers off (Pres
    #Power Scheme GUID: a666c91e-9613-4d84-a48e-2e4b7a016431 (Maximum Performa
    #Power Scheme GUID: e11a5899-9d8e-4ded-8740-628976fc3e63 (Video Playback)
    #9586a712-fcb4-4a06-af4b-52803dfbb9db = Performance

    $result = 0
    if ($PlanName -eq 'Custom') {
        if (Test-Path -Path $FileName) {
            POWERCFG -IMPORT $FileName
        }
        else {
            Write-Warning "Power Config file not found: $FileName"
            $result = -1
        }
    }
    else {
        switch ($PlanName) {
            'HighPerformance' {
                $ppguid = '8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c'
            }
            'Performance' {
                $ppguid = '9586a712-fcb4-4a06-af4b-52803dfbb9db'
            }
            'Balanced' {
                $ppguid = '381b4222-f694-41f0-9685-ff5bb260df2e'
            }
            'PowerSaver' {
                $ppguid = 'a1841308-3541-4fab-bc81-f71556f20b4a'
            }
            'EnergyStar' {
                $ppguid = 'de7ef2ae-119c-458b-a5a3-997c2221e76e'
            }
        }
        $currentScheme = POWERCFG -GETACTIVESCHEME
        $currentScheme = $currentScheme.Split()
        if ($currentScheme[3] -ne $ppguid) {
            Write-Host "Current plan is $($currentScheme[5])"
            POWERCFG -SETACTIVE $ppguid
            $newScheme = POWERCFG -GETACTIVESCHEME
            $newScheme = $($newScheme.Split('(')[1]).Replace(')','')
            Write-Host "Active plan is now $newScheme"
        }
        else {
            Write-Host "Current plan is already $PlanName"
        }
    }
    Write-Output $result
}

function Disable-DsMachinePasswordSync {
    [CmdletBinding()]
    param()
    try {
        New-Item -Path HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon\Parameters -Name DisablePasswordChange -Value 1 -ItemType DWORD
    }
    catch {
        Write-Error $Error[0].Exception.Message
    }
}

function Add-DsTaskbarShortcut {
    [CmdletBinding()]
    param (
        [parameter(Mandatory=$True, HelpMessage="Target item to pin")]
        [ValidateNotNullOrEmpty()]
        [string] $Target
    )
    if (!(Test-Path $Target)) {
        Write-Warning "You freaking dumbass!!! $Target does not exist"
        break
    }

    $KeyPath1  = "HKCU:\SOFTWARE\Classes"
    $KeyPath2  = "*"
    $KeyPath3  = "shell"
    $KeyPath4  = "{:}"
    $ValueName = "ExplorerCommandHandler"
    $ValueData =
        (Get-ItemProperty `
            ("HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\" + `
                "CommandStore\shell\Windows.taskbarpin")
        ).ExplorerCommandHandler

    $Key2 = (Get-Item $KeyPath1).OpenSubKey($KeyPath2, $true)
    $Key3 = $Key2.CreateSubKey($KeyPath3, $true)
    $Key4 = $Key3.CreateSubKey($KeyPath4, $true)
    $Key4.SetValue($ValueName, $ValueData)

    $Shell = New-Object -ComObject "Shell.Application"
    $Folder = $Shell.Namespace((Get-Item $Target).DirectoryName)
    $Item = $Folder.ParseName((Get-Item $Target).Name)
    $Item.InvokeVerb("{:}")

    $Key3.DeleteSubKey($KeyPath4)
    if ($Key3.SubKeyCount -eq 0 -and $Key3.ValueCount -eq 0) {
        $Key2.DeleteSubKey($KeyPath3)
    }
}

<#
.SYNOPSIS
    Removes AppxPackages for current user only
.DESCRIPTION
    Removes AppxPackages for current user only
.PARAMETER PackageNames
    Array of Appx Package names
.EXAMPLE
    Remove-DsAppxPackages -Packages ('xbox','zune')
#>

function Remove-DsAppxPackages {
    [CmdletBinding()]
    param (
        [parameter()]
        [ValidateNotNullOrEmpty()]
        [string[]] $PackageNames = ('xbox','skype','zune','officehub','solitaire')
    )
    Write-Host "removing windows store apps for current user" -ForegroundColor cyan
    # use: (Get-AppxPackage).Name to display package names
    foreach ($pkg in $PackageNames) {
        Get-AppxPackage | Where-Object {$_.Name -match $pkg} | Remove-AppxPackage -ErrorAction SilentlyContinue
    }
}

<#
.SYNOPSIS
    Customize Start Menu and TaskBar
.DESCRIPTION
    (same)
.PARAMETER FeatureName
    Name of feature to configure or disable
.NOTES
https://www.howto-connect.com/registry-hacks-for-start-menu-and-taskbar-in-windows-10/
#>


function Set-DsWin10StartMenu {
    [CmdletBinding()]
    param (
        [parameter(Mandatory)]
        [ValidateSet('RecentApps','ContextMenu','PeopleIcon')]
        [string] $FeatureName
    )
    switch ($FeatureName) {
        'RecentApps' {
            New-Item -Path HKLM:\SOFTWARE\Policies\Microsoft\Windows\Explorer -Name HideRecentlyAddedApps -Value 1 -ItemType DWORD
        }
        'ContextMenu' {
            New-Item -Path HKLM:\SOFTWARE\Policies\Microsoft\Windows\Explorer -Name DisableContextMenusInStart -Value 0 -ItemType DWORD
        }
        'PeopleIcon' {
            New-Item -Path HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced\People -Name PeopleBand -Value 0 -ItemType DWORD
        }
    }
}