WindowsLab.psm1
#Requires -RunAsAdministrator <# .SYNOPSIS WindowsLab, tools to admin a Windows based Lab #> # Get path to config.json $configPath = Join-Path -Path $PSScriptRoot -ChildPath 'config.json' # Create an empty config.json file if missing if (-not (Test-Path -Path $configPath -PathType Leaf)) { # Empty JSON structure $emptyJson = @{ labPcNames = @() labPcMacs = @() } # Convert and save to JSON file $emptyJson | ConvertTo-Json -Depth 10 | Set-Content -Path $configPath } $config = $null # Script (module) scope variable function Watch-LabPcName { $script:config = Get-Content -Raw -Path $configPath | ConvertFrom-Json if ($script:config.labPcNames.Length -eq 0) { Write-Host "LabPc names not found. " -ForegroundColor Red Write-Host "Run Set-LabPcName to set LabPc names,then open a new shell and try again." Exit 126 # Command invoked cannot execute } } function Test-LabPcPrompt { <# .SYNOPSIS Tests for each LabPC if the WinRM service is running. .DESCRIPTION This cmdlet informs you which LabPCs are ready to accept cmdlets from Main computer. .EXAMPLE Test-LabPcPrompt #> [CmdletBinding()] param () Watch-LabPcName foreach ($pc in $script:config.labPcNames) { 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 Sync-LabPcDate { <# .SYNOPSIS Sync the date with the NTP time for each computer. .EXAMPLE Sync-LabPcDate .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 () Watch-LabPcName # 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 $script:config.labPcNames -ScriptBlock { Set-Date -Date $Using:currentDate | Out-Null Write-Host "$env:computername synchronized" -ForegroundColor Green } } catch { Write-Host "`nTry again later ..." -ForegroundColor Yellow } } function Deploy-Item { <# .SYNOPSIS Deploy a file or folder from AdminPC to LabPCs .DESCRIPTION Copy a file or folder to all LabUser desktops, folders are copied recursively. #> [CmdletBinding()] param( [Parameter(Mandatory=$True, HelpMessage="Enter Path to file or folder")] [string]$Path, [Parameter(Mandatory=$True, HelpMessage="Enter LabUser name")] [string]$UserName ) Watch-LabPcName Resolve-Path -Path $Path -ErrorAction Stop | Out-Null $script:config.labPcNames | ForEach-Object -Parallel { $session = New-PSSession -ComputerName $_ $labUserprofilePath = Invoke-Command -Session $session -ScriptBlock { param($UName) try { # LabUser exist? $labUser = Get-LocalUser -Name $UName -ErrorAction Stop # LabUser signed-in? $labUserProfilePath = (Get-CimInstance -Class Win32_UserProfile | Where-Object { $_.SID -eq $labUser.SID.Value }).LocalPath if ($null -eq $labUserProfilePath) { Write-Host "$UName exist but never signed-in on $env:computername" -ForegroundColor Yellow Write-Host "Deployment to $env:computername failed" -ForegroundColor Red } } catch [Microsoft.PowerShell.Commands.UserNotFoundException] { Write-Host "$UName NOT exist on $env:computername" -ForegroundColor Yellow Write-Host "Deployment to $env:computername failed" -ForegroundColor Red $labUserProfilePath = $null } finally { $labUserProfilePath } } -ArgumentList $using:UserName if ($null -ne $labUserprofilePath) { $labUserDesktopPath = Join-Path -Path $labUserprofilePath -ChildPath 'Desktop' Copy-Item -Path $using:Path -Destination $labUserDesktopPath -ToSession $session -Recurse -Force Write-Host "Deployment to $_ success" -ForegroundColor Green } Remove-PSSession $session } -ThrottleLimit 5 } function Disconnect-User { <# .SYNOPSIS Disconnect any connected user from each LabPC .EXAMPLE Disconnect-User .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() Watch-LabPcName Invoke-Command -ComputerName $script:config.labPcNames -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 } } } # -- LabUser section -- 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 ) Watch-LabPcName Invoke-Command -ComputerName $script:config.labPcNames -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 ) Watch-LabPcName Invoke-Command -ComputerName $script:config.labPcNames -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(DefaultParameterSetName = 'Set0', SupportsShouldProcess = $True)] param ( [Parameter(Mandatory=$True, HelpMessage="Enter the username for Lab User")] [string]$UserName, [switch]$SetPassword, [validateSet('StandardUser', 'Administrator')] [string]$AccountType, [Parameter(ParameterSetName = 'Set1')] [switch]$BackupDesktop, [Parameter(ParameterSetName = 'Set2')] [switch]$RestoreDesktop ) Watch-LabPcName switch ($PSCmdlet.ParameterSetName) { 'Set0' {$password = $null if ($SetPassword.IsPresent) { # Prompt and read new password $password = Read-Host -Prompt 'Enter the new password' -AsSecureString } Invoke-Command -ComputerName $script:config.labPcNames -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 } } } 'Set1' {Backup-LabUserDesktop -UserName $UserName} # -BackupDesktop provided 'Set2' {Restore-LabUserDesktop -UserName $UserName} # -RestoreDesktop provided } } function Backup-LabUserDesktop { <# .SYNOPSIS Back up LabUser desktop into ROOT:\LabPc folder .DESCRIPTION This cmdlet copies LabUser desktop files and folders into into ROOT:|LabPc folder and deletes any previous item. .EXAMPLE Backup-LabUserDesktop -UserName Alunno #> [CmdletBinding()] param( [Parameter(Mandatory=$True, HelpMessage="Enter LabUser name")] [string]$UserName ) invoke-Command -ComputerName $script:config.labPcNames -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 LabPc folder if not exist $labPcPath = Join-Path -Path $env:SystemDrive -ChildPath 'LabPc' New-Item -Path $labPcPath -ItemType "directory" -ErrorAction SilentlyContinue # copy labuser desktop Remove-Item -Path $labPcPath -Force -Recurse -ErrorAction SilentlyContinue # delete any previous saved desktop Copy-Item -Path "$userDesktopPath\" -Destination $labPcPath -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-LabUserDesktop { <# .SYNOPSIS Restore LabUser desktop backup from ROOT:\LabPc .DESCRIPTION This cmdlet copies back the LabUser desktop backup from ROOT:\LabPc folder, overwrite any existing items. .EXAMPLE Restore-LabUserDesktop -UserName Alunno #> [CmdletBinding()] param( [Parameter(Mandatory=$True, HelpMessage="Enter LabUser name")] [string]$UserName ) invoke-Command -ComputerName $script:config.labPcNames -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 "LabPc" 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 } } } # -- LabPc section -- function Show-LabPcMac { <# .SYNOPSIS Show info about ethernet ComputerLab MAC addresses .DESCRIPTION Show-LabPcMac searchs for Ethernet (wired LAN) MAC addresses for later use with WoL in Start-LabPc cmdlet, #> Watch-LabPcName Write-Host "Searching for physical, connected, ethernet net adapter MAC addresses ..." -ForegroundColor DarkYellow $MACs = @() $script:config.labPcNames | ForEach-Object { try { Write-Host "$_ " -ForegroundColor DarkYellow -NoNewline # Search for Physical, connected (Up), ethernet (standard 802.3) adapter $netAdapter = Get-NetAdapter -Physical -CimSession $_ | Where-Object { $_.Status -eq "Up" -and ($_.PhysicalMediaType -like "*802.3*" -or $_.Name -like "*Ethernet*") } | Select-Object MacAddress if ($netAdapter.Length -eq 1) { # Found one case $MACs += $netAdapter.MacAddress Write-Host $netAdapter.MacAddress } else { # Found more then one case Write-Host "seams to have multiple ethernet net adapters, disconnect all but one" -NoNewline Write-Host $netAdapter.MacAddress -Separator ', ' } } catch [Microsoft.PowerShell.Cmdletization.Cim.CimJobException] { # Found none case Write-Host "Not yet reachable " -ForegroundColor Red -NoNewline Write-Host "(is computer on and connected via ethernet?)" -ForegroundColor DarkYellow } } $script:config.labPcMacs = $MACs if ($script:config.labPcNames.Length -eq $script:config.labPcMacs.Length) { # Save the updated JSON back to the file $script:config | ConvertTo-Json -Depth 10 | Set-Content -Path $configPath Write-Host "MAC addresses saved for use with Start-LabPc cmdlet." -ForegroundColor DarkYellow } else { Write-Host "Fix network adapter issues before using MAC addresses with Start-LabPc cmdlet." -ForegroundColor Red } } function Start-LabPc { <# .SYNOPSIS Turn on each computers if WoL setting is present and enabled in BIOS/UEFI .EXAMPLE Start-LabPc .NOTES https://www.pdq.com/blog/wake-on-lan-wol-magic-packet-powershell/ #> [CmdletBinding(SupportsShouldProcess)] param () Watch-LabPcName Write-Host "Start-LabPc works only if Computers support WoL. See documentation for details." -ForegroundColor DarkYellow if ($script:config.labPcNames.Length -eq $script:config.labPcMacs.Length) { # send Magic Packet over LAN foreach ($Mac in $script:config.labPcMacs) { $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() } } else { Write-Host "MAC address and computer name count mismatch. Run Show-LabPcMac to fix." -ForegroundColor Red } } function Stop-LabPc { <# .SYNOPSIS Force an immediate shut down of each computer .EXAMPLE Stop-LabPc .NOTES #> [CmdletBinding(DefaultParameterSetName = 'Set0', SupportsShouldProcess = $true)] param ( [Parameter(ParameterSetName = 'Set1')] [switch]$When, # Get scheduled LabPcs daily stops [Parameter(ParameterSetName = 'Set2')] [string]$DailyAt, # Schedule a new LabPc daily stop [Parameter(ParameterSetName = 'Set3')] [string]$NoMoreAt, # Remove a LabPc daily stop [Parameter(ParameterSetName = 'Set4')] [switch]$AndRestart # Restart LabPcs ) Watch-LabPcName switch ($PSCmdlet.ParameterSetName) { 'Set0' {Stop-Computer -ComputerName $script:config.labPcNames -Force} # no parameter provided 'Set1' {Get-LabPcStop} # -When provided 'Set2' {New-LabPcStop -DailyTime $DailyAt} # -DailyAt provided 'Set3' {Remove-LabPcStop -DailyTime $NoMoreAt} # -NoMoreAt provided 'Set4' {Restart-LabPc} # -AndRestart provided } } function Restart-LabPc { <# .SYNOPSIS Force an immediate restart of each computer and wait for them to be on again .EXAMPLE Restart-LabPc .NOTES #> [CmdletBinding(SupportsShouldProcess)] param() Restart-Computer -ComputerName $script:config.labPcNames -Force } function New-LabPcStop { <# .SYNOPSIS Schedule a new LabPC 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-LabPcStop -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 $script:config.labPcNames -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-LabPcStop { <# .SYNOPSIS Gets LabPC daily stops .DESCRIPTION This cmdlet gets all trigger times for StopThisComputer scheduled task .EXAMPLE Get-LabPcStop #> [CmdletBinding()] param () Invoke-Command -ComputerName $script:config.labPcNames -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-LabPcStop { <# .SYNOPSIS Removes a LabPC daily stop .DESCRIPTION This cmdlet removes if exist the trigger from StopThisComputer scheduled task with time -DailyTime .EXAMPLE Remove-LabPcStop -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 $script:config.labPcNames -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 } } } # -- GUI -- function Set-LabPcName { <# .SYNOPSIS GUI to manage LabPcs names .DESCRIPTION Allows to set/update config.json file through a GUI #> [CmdletBinding(SupportsShouldProcess)] param() # Load the Windows Forms assembly Add-Type -AssemblyName System.Windows.Forms Add-Type -AssemblyName System.Drawing # Create the form $form = New-Object System.Windows.Forms.Form $form.Text = "Lab Settings" $form.Size = New-Object System.Drawing.Size(500, 175) # Reduced height # Create a label for the computer names $labelNames = New-Object System.Windows.Forms.Label $labelNames.Text = "Set LabPcs Names (comma-separated):" $labelNames.AutoSize = $true $labelNames.Location = New-Object System.Drawing.Point(10, 10) $form.Controls.Add($labelNames) # Create a TextBox to display and edit the computer names $textboxNames = New-Object System.Windows.Forms.TextBox $textboxNames.Multiline = $false $textboxNames.ScrollBars = 'Horizontal' $textboxNames.Size = New-Object System.Drawing.Size(465, 30) # Increased width $textboxNames.Location = New-Object System.Drawing.Point(10, 35) $form.Controls.Add($textboxNames) # Create Save button $saveButton = New-Object System.Windows.Forms.Button $saveButton.Text = "Save" $saveButton.Location = New-Object System.Drawing.Point(10, 100) # Moved buttons down to create more room $form.Controls.Add($saveButton) # Create Refresh button $refreshButton = New-Object System.Windows.Forms.Button $refreshButton.Text = "Refresh" $refreshButton.Location = New-Object System.Drawing.Point(100, 100) # Moved buttons down to create more room $form.Controls.Add($refreshButton) # Create a status label for success/failure messages $statusLabel = New-Object System.Windows.Forms.Label $statusLabel.AutoSize = $true $statusLabel.Location = New-Object System.Drawing.Point(10, 75) $form.Controls.Add($statusLabel) # Save JSON function $saveButton.Add_Click({ try { # Get the updated computer names from the textbox (comma-separated) $newNames = $textboxNames.Text -split ",\s*" # Silently remove empty values $newNames = $newNames | Where-Object { $_ -ne "" } # Cast to array to avoid PowerShell treating a single element as a string $newNames = $newNames -as [System.Array] # Load the original JSON, update the 'labPcNames' key with new values $script:config = Get-Content -Path $configPath -Raw | ConvertFrom-Json $script:config.labPcNames = $newNames # Save the updated JSON back to the file $script:config | ConvertTo-Json -Depth 10 | Set-Content -Path $configPath # Show success message in green $statusLabel.Text = "Settings saved successfully." $statusLabel.ForeColor = 'Green' } catch { # Show error message in red $statusLabel.Text = "Failed to save settings." $statusLabel.ForeColor = 'Red' } }) # Refresh function (reloading JSON) $refreshButton.Add_Click({ Import-JsonContent }) # Load the JSON content as soon as the form pops up $form.Add_Shown({ Import-JsonContent # Then load the content }) # Show the form $form.ShowDialog() } # Function to load JSON content function Import-JsonContent { try { # Read the JSON file content $script:config = Get-Content -Path $configPath -Raw | ConvertFrom-Json # Extract and display values from the 'labPcNames' key (array of values) $names = $script:config.labPcNames -join ", " $textboxNames.Text = $names # Clear status label on successful load $statusLabel.Text = "" } catch { # Show error message in red $statusLabel.Text = "Failed to load JSON." $statusLabel.ForeColor = 'Red' } } |