WindowsLab.psm1

#Requires -RunAsAdministrator
<#
.SYNOPSIS
    ComputerRoom tools
 
.NOTES
    Script variables are essentially module variables
#>


# get path to config.json
$configPath = Join-Path -Path $PSScriptRoot -ChildPath 'config.json'

if (Test-Path -Path $configPath -PathType Leaf) {
    # read config file
    $config = Get-Content -Raw -Path $configPath | ConvertFrom-Json

    $labComputerList = $config.labComputerList
    $macs = $config.macs
} else {
    $t = "
===========================================
--- WinLabAdmin Module ---
===========================================
"

    Write-Host $t.Trim() -ForegroundColor DarkYellow
    Write-Host "config.json not found at the following path:" -ForegroundColor Red
    Write-Host $PSScriptRoot -ForegroundColor DarkYellow
    Write-Host "1. rename config.json.example to config.json"
    Write-Host "2. update config.json with lab computer names in your lab. Ignore Mac Addresses at this step"
    Write-Host "3. open a new shell and try again"
    Exit 2 # file not found
}

function Show-Config {
    <#
    .SYNOPSIS
        Shows imported config.json content and if mac addresses are missing get them for you
 
    .EXAMPLE
        Read-Config
 
    .NOTES
        Filtering the adapters with status: `Up` should be enaught to select the
        adapter used for PS Remoting
    #>

    [CmdletBinding()]
    param ()
    $t = "
===========================================
--- Imported from config.json ---
===========================================
"


    Write-Host $t.Trim() -ForegroundColor DarkYellow

    Write-Host 'Lab Computer: ' -NoNewline
    Write-Host $labComputerList -Separator ', '

    Write-Host 'Mac Addresses : ' -NoNewline
    Write-Host $macs -Separator ', '

    $t = "
===========================
-- Mac Addresses check --
===========================
"


    Write-Host $t.Trim() -ForegroundColor DarkYellow
    Write-Host 'Trying to find Mac Addresses ...'
    foreach ($pc in $labComputerList) {
        $macAddress = Get-NetAdapter -CimSession $PC | Where-Object {$_.Status -eq 'Up'} | Select-Object MacAddress

        Write-Host "$pc " -ForegroundColor DarkYellow -NoNewline
        if ($macAddress.Length -gt 1) {
            Write-Host $macAddress.MacAddress, "=== $($macAddress.Length) Net Adapters here, choose one ===" -Separator ', '
        } else {
            Write-Host $macAddress.MacAddress
        }
    }

}


function Start-LabComputer {
    <#
    .SYNOPSIS
        Turn on each computers if WoL setting is present and enabled in BIOS/UEFI
 
    .EXAMPLE
        Start-LabComputer
 
    .NOTES
        https://www.pdq.com/blog/wake-on-lan-wol-magic-packet-powershell/
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param ()
    # send Magic Packet over LAN
    foreach ($Mac in $macs) {
        $MacByteArray = $Mac -split "[:-]" | ForEach-Object { [Byte] "0x$_"}
        [Byte[]] $MagicPacket = (,0xFF * 6) + ($MacByteArray * 16)
        $UdpClient = New-Object System.Net.Sockets.UdpClient
        $UdpClient.Connect(([System.Net.IPAddress]::Broadcast),7)
        $UdpClient.Send($MagicPacket,$MagicPacket.Length)
        $UdpClient.Close()
    }

}

function Restart-LabComputer {
    <#
    .SYNOPSIS
        Force an immediate restart of each computer and wait for them to be on again
 
    .EXAMPLE
        Restart-LabComputer
 
    .NOTES
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param()
    Restart-Computer -ComputerName $labComputerList -Force
}

function Stop-LabComputer {
    <#
    .SYNOPSIS
        Force an immediate shut down of each computer
 
    .EXAMPLE
        Stop-LabComputer
 
    .NOTES
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param()
    Stop-Computer -ComputerName $labComputerList -Force
}

function Disconnect-AnyUser {
    <#
    .SYNOPSIS
        Disconnect any connected user from each Lab computer
 
    .EXAMPLE
        Disconnect-AnyUser
 
    .NOTES
        Windows Home edition doesn't include query.exe (https://superuser.com/a/1646775)
 
        Quser.exe emit a non-terminating error in case of no user logged-in,
        to catch the error force PS to raise an exception, set $ErrorActionPreference = 'Stop'
        because quser, being not a cmdlet, has not -ErrorAction parameter.
    #>

    [CmdletBinding()]
    param()

    Invoke-Command -ComputerName $labComputerList -ScriptBlock {
        $ErrorActionPreference = 'Stop' # NOTE: it is valid only for this function scope
        try {
            # check if quser command exist
            Get-Command -Name quser -ErrorAction Stop | Out-Null

            # get array of logged-in users, skip 1st row (the head)
            quser | Select-Object -Skip 1 |
            ForEach-Object {
                # logoff by session ID
                logoff ($_ -split "\s+")[2]
                Write-Host "User", ($_ -split "\s+")[1], "logged out $($env:COMPUTERNAME)"  -ForegroundColor Green
            }
        }
        catch [System.Management.Automation.CommandNotFoundException] {
            Write-Host "Cannot disconnect any user: quser command not found on $env:computername" -ForegroundColor Red
            Write-Host "is it a windows Home edition?"
        }
        catch {
            Write-host "No user logged in $($env:COMPUTERNAME)" -ForegroundColor Yellow
        }
    }
}

function New-LabUser {
    <#
    .SYNOPSIS
        Create a Standard Lab user with a blank never-expiring password
 
    .EXAMPLE
        New-LabUser -UserName "Alunno"
 
    .NOTES
        I just want to clarify the usage of the New-LocalUser cmdlet's switch parameters
        -NoPassword and -UserMayNotChangePassword. According to Microsoft, the -NoPassword
        parameter indicates that the user account doesn't have a password. However, in my
        tests, the user was prompted to provide a password when signing in for the first time.
        This indicates that -NoPassword is different from a blank password. Consequently, using
        -NoPassword along with -UserMayNotChangePassword results in a deadlock.
 
        Windows Groups' description: https://ss64.com/nt/syntax-security_groups.html
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param (
      [Parameter(Mandatory=$True, HelpMessage="Enter username for Lab User")]
      [string]$UserName
    )

    Invoke-Command -ComputerName $labComputerList -ScriptBlock {
        try {
            $blankPassword = [securestring]::new()
            New-LocalUser -Name $Using:UserName -Password $blankPassword -PasswordNeverExpires `
            -UserMayNotChangePassword -AccountNeverExpires -ErrorAction Stop | Out-Null

            Add-LocalGroupMember -Group "Users" -Member $Using:UserName

            Write-Host "$Using:UserName created on $env:computername" -ForegroundColor Green
        }
        catch [Microsoft.PowerShell.Commands.UserExistsException] {
          Write-Host "$Using:UserName already exist on $env:computername" -ForegroundColor Yellow
        }
    }
}

function Remove-LabUser {
    <#
    .SYNOPSIS
        Remove specified Lab User, also remove registry entry and user profile folder if they exist
 
    .DESCRIPTION
        This cmdlet log out the lab user if he is logged in and completely remove it
 
    .EXAMPLE
        Remove-LabUser -Username "Alunno"
 
    .NOTES
        Inspiration: https://adamtheautomator.com/powershell-delete-user-profile/
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param (
      [Parameter(Mandatory=$True, HelpMessage="Enter username for Lab User")]
      [string]$UserName
    )

    Invoke-Command -ComputerName $labComputerList -ScriptBlock {
        try {
            # check if quser command exist
            Get-Command -Name quser -ErrorAction Stop | Out-Null

            # log out if logged in otherwise silently continue
            $ErrorActionPreference = 'SilentlyContinue'
            quser $Using:UserName | Select-Object -Skip 1 |
            ForEach-Object {
                # logoff by session ID
                logoff ($_ -split "\s+")[2]
                Write-Host "User", ($_ -split "\s+")[1], "logged out $($env:COMPUTERNAME)"  -ForegroundColor Green
            }
            $ErrorActionPreference = 'Continue'
        }
        catch [System.Management.Automation.CommandNotFoundException] {
            Write-Host "quser command not found on $env:computername" -ForegroundColor Red
            Write-Host "is it a windows Home edition? I'll try to remove $using:UserName anyway ...`n"
        }

        try {
            $localUser = Get-LocalUser -Name $Using:UserName -ErrorAction Stop

            # Remove the sign-in entry in Windows
            Remove-LocalUser -SID $localUser.SID.Value

            # Remove %USERPROFILE% folder and registry entry if exist
            Get-CimInstance -Class Win32_UserProfile | Where-Object { $_.SID -eq $localUser.SID.Value } | Remove-CimInstance

            Write-Host "$Using:UserName removed on $env:computername" -ForegroundColor Green
        }
        catch [Microsoft.PowerShell.Commands.UserNotFoundException] {
            <#Do this if a terminating exception happens#>
            Write-Host "$Using:UserName NOT exist on $env:computername" -ForegroundColor Yellow
        }
    }
}

function Set-LabUser {
    <#
    .SYNOPSIS
        Set password and account type for the LabUser specified
 
    .EXAMPLE
        Set-LabUser -UserName "Alunno"
        Set-LabUser -UserName "Alunno" -SetPassword
        Set-LabUser -UserName "Alunno" -SetPassword -AccountType Administrator
 
    .NOTES
        LabUser Administrators can't change the password like standard users
 
        Windows Groups description: https://ss64.com/nt/syntax-security_groups.html
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory=$True, HelpMessage="Enter the username for Lab User")]
        [string]$UserName,
        [switch]$SetPassword,
        [validateSet('StandardUser', 'Administrator')]
        [string]$AccountType
    )

    $password = $null
    if ($SetPassword.IsPresent) {
        # Prompt and read new password
        $password = Read-Host -Prompt 'Enter the new password' -AsSecureString
    }
    Invoke-Command -ComputerName $labComputerList  -ScriptBlock {
        try {
            if ($Using:SetPassword.IsPresent) {
                # change password
                Set-LocalUser -Name $Using:UserName -Password $Using:Password -PasswordNeverExpires $True `
                -UserMayChangePassword $False -ErrorAction Stop
                Write-Host "$Using:UserName on $env:computername password changed" -ForegroundColor Green
            }
            if ($Using:AccountType -eq 'Administrator') {
                # change to an Administrator
                Add-LocalGroupMember -Group "Administrators" -Member $Using:UserName -ErrorAction Stop
                Write-Host "$Using:UserName on $env:computername is now an Administrator" -ForegroundColor Green
            }
            if ($Using:AccountType -eq 'StandardUser') {
                # change to a Standard User
                Remove-LocalGroupMember -Group "Administrators" -Member $Using:UserName -ErrorAction Stop
                Write-Host "$Using:UserName on $env:computername is now a Standard User" -ForegroundColor Green
            }
        }
        catch [Microsoft.PowerShell.Commands.UserNotFoundException] {
            Write-Host "$Using:UserName NOT exist on $env:computername" -ForegroundColor Yellow
        }
        catch [Microsoft.PowerShell.Commands.MemberExistsException] {
            Write-Host "$Using:UserName on $env:computername is already an Administrator" -ForegroundColor Yellow
        }
        catch [Microsoft.PowerShell.Commands.MemberNotFoundException] {
            Write-Host "$Using:UserName on $env:computername is already a Standard User" -ForegroundColor Yellow
        }
        catch {
            $_.exception.GetType().fullname
        }
    }
}

function Sync-LabComputerDate {
    <#
    .SYNOPSIS
        Sync the date with the NTP time for each computer.
 
        .EXAMPLE
        Sync-LabComputerDate
 
    .NOTES
        The NtpTime module is required on MasterComputer (https://www.powershellgallery.com/packages/NtpTime/1.1)
 
        Set-Date requires admin privilege to run
    #>

    [CmdletBinding()]
    param ()
    # check if NtpTime module is installed
    if ($null -eq (Get-Module -ListAvailable -Name NtpTime)) {
        Write-Host "`nNtpTime Module missing. Install the module with:" -ForegroundColor Yellow
        Write-Host " Install-Module -Name NtpTime`n"
        Break
    }

    # get datetime from default NTP server
    try {
        $currentDate = (Get-NtpTime -MaxOffset 60000).NtpTime
        Write-Host "`n(NTP time: $currentdate)`n" -ForegroundColor Yellow

        Set-Date -Date $currentDate | Out-Null
        Write-Host "MasterComputer synchronized" -ForegroundColor Green
        Invoke-Command -ComputerName $labComputerList -ScriptBlock {
            Set-Date -Date $Using:currentDate | Out-Null
            Write-Host "$env:computername synchronized" -ForegroundColor Green
        }
    }
    catch {
        Write-Host "`nTry again later ..." -ForegroundColor Yellow
    }
}

function Copy-ToLabUserDesktop {
    <#
    .SYNOPSIS
        Copy a file or folder from one location to LabUser Desktop
 
    .DESCRIPTION
        Copy a file or folder from one location to LabUser Desktop, folders are copied recursively.
        This cmdlet can copy over a read-only file or alias.
 
    .EXAMPLE
        Copy-ToLabUserDesktop -Path filename.txt -UserName Alunno
 
        Copy-ToLabUserDesktop -Path C:\Logfiles -UserName Concorso
 
    .NOTES
        Inspiration: https://lazyadmin.nl/powershell/copy-file/#copy-file-to-remote-computer-with-powershell
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$True, HelpMessage="Enter Path to file or folder")]
        [string]$Path,
        [Parameter(Mandatory=$True, HelpMessage="Enter LabUser name")]
        [string]$UserName
    )

    Write-Host "Start copying to LabUser Desktops ..." -ForegroundColor Yellow

    foreach ($computerName in $labComputerList) {
        $session = New-PSSession -ComputerName $computerName

            $userprofile = Invoke-Command -Session $session -ScriptBlock {
                try {
                    $localUser = Get-LocalUser -Name $Using:UserName -ErrorAction Stop

                    # Get %USERPROFILE% path
                    $userprofile = (Get-CimInstance -Class Win32_UserProfile | Where-Object { $_.SID -eq $localUser.SID.Value }).LocalPath
                    if ($null -eq $userprofile) {
                        Write-Host "$Using:UserName exist but never signed-in on $env:computername" -ForegroundColor Yellow
                        Write-Host "Copy to $env:computername failed" -ForegroundColor Red
                        $userprofile = ""
                    }
                }
                catch [Microsoft.PowerShell.Commands.UserNotFoundException] {
                    Write-Host "$Using:UserName NOT exist on $env:computername" -ForegroundColor Yellow
                    Write-Host "Copy to $env:computername failed" -ForegroundColor Red
                    $userprofile = $null
                }
                finally {
                    Write-Output $userprofile
                }

            }
            if ($userprofile -ne "" -and $null -ne $userprofile) {
                $desktopPath = Join-Path -Path $userprofile -ChildPath 'Desktop'
                Copy-Item -Path $path -Destination $desktopPath -ToSession $session -Recurse -Force
                Write-host "copy to $computerName success" -ForegroundColor Green
            }

        Remove-PSSession -Session $session
    }
}

function Test-LabComputerPrompt {
    <#
    .SYNOPSIS
        Tests for each Lab computer if the WinRM service is running.
 
    .DESCRIPTION
        This cmdlet informs you which Lab computers are ready to accept cmdlets from Main computer.
 
    .EXAMPLE
        Test-LabComputerPrompt
    #>

    [CmdletBinding()]
    param ()

    foreach ($pc in $labComputerList) {
        try {
            Test-WSMan -ComputerName $pc -ErrorAction Stop | Out-Null
            Write-Host "$pc " -ForegroundColor DarkYellow -NoNewline
            Write-Host "ready" -ForegroundColor Green
        }
        catch [System.InvalidOperationException] {
            Write-Host "$pc " -ForegroundColor DarkYellow -NoNewline
            Write-Host "not ready" -ForegroundColor Red
        }
    }
}

function Save-LabComputerDesktop {
    <#
    .SYNOPSIS
        Save a copy of Lab user desktop folder into the Lab computer root
 
    .DESCRIPTION
        This cmdlet copies the Lab user desktop folder into into ROOT/LabComputer folder and deletes any previous item.
 
    .EXAMPLE
        Save-LabComputerDesktop -UserName Alunno
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$True, HelpMessage="Enter LabUser name")]
        [string]$UserName
    )
    invoke-Command -ComputerName $labComputerList -ScriptBlock {
        try {
            # get specified Lab user
            $localUser = Get-LocalUser -Name $Using:UserName -ErrorAction Stop

            # get Lab user USERPROFILE path
            $userProfilePath = (Get-CimInstance -Class Win32_UserProfile | Where-Object { $_.SID -eq $localUser.SID.Value }).LocalPath
            Test-Path -Path $userProfilePath -ErrorAction Stop | Out-Null

            $userDesktopPath = Join-Path -Path $userprofilePath -ChildPath 'Desktop'

            # create LabComputer folder if not exist
            $labComputerPath = Join-Path -Path $env:SystemDrive -ChildPath 'LabComputer'
            New-Item -Path $labComputerPath -ItemType "directory" -ErrorAction SilentlyContinue

            # copy lab user desktop
            $destinationPath = Join-Path -Path $labComputerPath -ChildPath "$Using:UserName-Desktop"
            Remove-Item -Path $destinationPath -Force -Recurse -ErrorAction SilentlyContinue # delete previous saved desktop if any
            Copy-Item -Path "$userDesktopPath\" -Destination $destinationPath -Recurse -Force

            Write-Host "$Using:Username Desktop saved for $env:computername" -ForegroundColor Green
        }
        catch [Microsoft.PowerShell.Commands.UserNotFoundException] {
            Write-Host "$Using:UserName @ $env:computername does NOT exist" -ForegroundColor Yellow
            Write-Host "$Using:Username Desktop save failed for $env:computername" -ForegroundColor Red
        }
        catch [System.Management.Automation.ParameterBindingException] {
            # user exist USERPROFILE path no
            Write-Host "$Using:UserName exist but never signed-in on $env:computername" -ForegroundColor Yellow
            Write-Host "$Using:Username Desktop save failed for $env:computername" -ForegroundColor Red
        }
    }
}

function Restore-LabComputerDesktop {
    <#
    .SYNOPSIS
        Restore the copy of Lab user desktop folder from the Lab computer root
 
    .DESCRIPTION
        This cmdlet copies back the Lab user desktop folder from into ROOT/LabComputer folder, overwrite any existing items.
 
    .EXAMPLE
        Restore-LabComputerDesktop -UserName Alunno
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$True, HelpMessage="Enter LabUser name")]
        [string]$UserName
    )
    invoke-Command -ComputerName $labComputerList -ScriptBlock {
        try {
            # get specified Lab user
            $localUser = Get-LocalUser -Name $Using:UserName -ErrorAction Stop

            # get Lab user USERPROFILE path
            $userProfilePath = (Get-CimInstance -Class Win32_UserProfile | Where-Object { $_.SID -eq $localUser.SID.Value }).LocalPath
            Test-Path -Path $userProfilePath -ErrorAction Stop | Out-Null

            $userDesktopPath = Join-Path -Path $userprofilePath -ChildPath 'Desktop'

            # copy lab user desktop back
            $sourcePath = Join-Path -Path $env:SystemDrive -ChildPath "LabComputer" | Join-Path -ChildPath "$using:UserName-Desktop"
            Copy-Item -Path "$sourcePath\*" -Destination $userDesktopPath -Recurse -Force

            Write-Host "$Using:Username Desktop restored for $env:computername" -ForegroundColor Green
        }
        catch [Microsoft.PowerShell.Commands.UserNotFoundException] {
            Write-Host "$Using:UserName @ $env:computername does NOT exist" -ForegroundColor Yellow
            Write-Host "$Using:Username Desktop restore failed for $env:computername" -ForegroundColor Red
        }
        catch [System.Management.Automation.ParameterBindingException] {
            Write-Host "$Using:UserName exist but never signed-in on $env:computername" -ForegroundColor Yellow
            Write-Host "$Using:Username Desktop restore failed for $env:computername" -ForegroundColor Red
        }
    }
}

function New-LabComputerStop {
    <#
    .SYNOPSIS
        Schedule a new Lab Computer daily stop
 
    .DESCRIPTION
        This cmdlet creates the new task StopThisComputer and if the task already exist, just adds the new stop time as a trigger to the task
 
    .EXAMPLE
        New-LabComputerStop -DailyTime '14:15'
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory=$True, HelpMessage="Enter the daily stop time")]
        [string]$DailyTime
    )

    # Time parameter parsing
    try {
        $dailyTimeObj = [DateTime]::ParseExact($DailyTime, "HH:mm", [System.Globalization.CultureInfo]::InvariantCulture)
    }
    catch {
        Write-Error "-DailyTime $DailyTime must be in HH:mm format"
        return $null
    }

    # Convert $DailyTimeObj to a TimeSpan object
    $dailyStopTime = $dailyTimeObj.TimeOfDay

    # Set the new daily stop time trigger
    $trigger = New-ScheduledTaskTrigger -Daily -At $dailyTimeObj

    # Set the action
    $action = New-ScheduledTaskAction -Execute 'Powershell' -Argument '-NoProfile -ExecutionPolicy Bypass -Command "& {Stop-Computer -Force}"'

    Invoke-Command -ComputerName $labComputerList -ScriptBlock {

        # Set principal contex for SYSTEM account to run as a service with with the highest privileges
        $principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest

        try {
            # Get scheduled StopThisComputer task if exist
            $stopThisComputerTask = Get-ScheduledTask -TaskName:'StopThisComputer' -TaskPath:'\WinLabAdmin\' -ErrorAction Stop
        }
        catch [Microsoft.PowerShell.Cmdletization.Cim.CimJobException] {
            # Register the task (-TaskPath is the folder)
            Register-ScheduledTask -TaskName:'StopThisComputer' -TaskPath:'\WinLabAdmin\' -Action $using:action -Trigger $using:trigger -Principal $principal | Out-Null
            Write-Host "First stop daily time $using:DailyTime just set on $env:computername" -ForegroundColor Green
            Write-Host " ... and StopThisComputer task set`n"
            Return $null
        }

        # Get preset daily stop times as TimeSpan objets
        $presetDailyStopTimes = @()
        foreach ($trg in $stopThisComputerTask.Triggers) {
            $presetDailyStopTimes += ([datetime] $trg.StartBoundary).TimeOfDay
        }

        # Check if the new stop time is already set
        if ($using:dailyStopTime -in $presetDailyStopTimes) {
            Write-Host "Stop at daily time $using:DailyTime already exist on $env:computername" -ForegroundColor Red
        } else {
            # Add the new stop time
            $stopThisComputerTask.Triggers += $using:trigger
            Set-ScheduledTask -TaskName:'StopThisComputer' -TaskPath:'\WinLabAdmin\' -Trigger $stopThisComputerTask.Triggers -Principal $principal | Out-Null
            Write-Host "New stop at daily time $using:DailyTime added to $env:computername" -ForegroundColor Green
        }

    }
}

function Get-LabComputerStop {
    <#
    .SYNOPSIS
        Gets Lab computer daily stops
 
    .DESCRIPTION
        This cmdlet gets all trigger times for StopThisComputer scheduled task
 
    .EXAMPLE
        Get-LabComputerStop
    #>

    [CmdletBinding()]
    param ()

    Invoke-Command -ComputerName $labComputerList -ScriptBlock {

        $formattedTime = "`n${env:COMPUTERNAME}:`n "
        try {
            # Get scheduled StopThisComputer task if exist
            $stopThisComputerTask = Get-ScheduledTask -TaskName:'StopThisComputer' -TaskPath:'\WinLabAdmin\' -ErrorAction Stop
        }
        catch [Microsoft.PowerShell.Cmdletization.Cim.CimJobException] {
            # $_.exception.GetType().fullname
            $formattedTime += "None"
            Write-Host $formattedTime
            Return $null
        }

        # Get preset daily stop times as TimeSpan objets
        $presetDailyStopTimes = @()
        foreach ($trg in $stopThisComputerTask.Triggers) {
            $presetDailyStopTimes += ([datetime] $trg.StartBoundary).TimeOfDay
        }

        # Print the array in "hh:mm" format
        foreach ($timeSpan in $presetDailyStopTimes) {
            $formattedTime += "{0:hh\:mm\,\ }" -f $timeSpan
        }
        $formattedTime = $formattedTime.Substring(0, $formattedTime.Length - 2)
        Write-Host $formattedTime
    }
}

function Remove-LabComputerStop {
    <#
    .SYNOPSIS
        Removes a Lab computer daily stop
 
    .DESCRIPTION
        This cmdlet removes if exist the trigger from StopThisComputer scheduled task with time -DailyTime
 
    .EXAMPLE
        Remove-LabComputerStop -DailyTime '14:14'
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory=$True, HelpMessage="Enter daily stop time to remove")]
        [string]$DailyTime
    )

    # Time parameter parsing
    try {
        $dailyTimeObj = [DateTime]::ParseExact($DailyTime, "HH:mm", [System.Globalization.CultureInfo]::InvariantCulture)
    }
    catch {
        Write-Error "-DailyTime $DailyTime must be in HH:mm format"
        return $null
    }

    # Convert $DailyTimeObj to a TimeSpan object
    $dailyStopTime = $dailyTimeObj.TimeOfDay

    Invoke-Command -ComputerName $labComputerList -ScriptBlock {

        try {
            # Get scheduled StopThisComputer task if exist
            $stopThisComputerTask = Get-ScheduledTask -TaskName:'StopThisComputer' -TaskPath:'\WinLabAdmin\' -ErrorAction Stop
        }
        catch [Microsoft.PowerShell.Cmdletization.Cim.CimJobException] {
            # $_.exception.GetType().fullname
            Write-Host "Stop daily time $Using:DailyTime not exist on $env:computername" -ForegroundColor Red
            Return $null
        }

        # Set principal contex for SYSTEM account to run as a service with with the highest privileges
        $principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest

        # Remove the given time stop trigger
        $triggers = @()
        foreach ($trg in $stopThisComputerTask.Triggers) {
            if (([datetime] $trg.StartBoundary).TimeOfDay -ne $Using:dailyStopTime) {
                $triggers += $trg
            }
        }


        if ($triggers.Count -eq 0) {
            Unregister-ScheduledTask -TaskName:'StopThisComputer' -TaskPath:'\WinLabAdmin\' -Confirm:$false
            Write-Host "Last Stop daily time $Using:DailyTime removed on $env:computername" -ForegroundColor Green
            Write-Host " ... and StopThisComputer Task deleted`n"
        }
        elseif ($triggers.count -lt $stopThisComputerTask.Triggers.count) {
            Set-ScheduledTask -TaskName:'StopThisComputer' -TaskPath:'\WinLabAdmin\' -Trigger $triggers -Principal $principal | Out-Null
            Write-Host "Stop daily time $Using:DailyTime removed on $env:computername" -ForegroundColor Green
        } else {
            Write-Host "Stop daily time $Using:DailyTime not exist on $env:computername" -ForegroundColor Red
        }

    }
}