UPDATE
I'm not struggeling with this error An event with the name 'StateChanged' does not exist - See the full code at TRYOUTS (the last code piece) below
original text
As mentioned before, I'm doing a code for automatically format and transfer files to multiple USB thumbdrives. And it´s done in parallel/async
Now the issue is that I want to output the drive letter of each USB as they completes / job is done. That letter is stored in a var/param inside the job function, but I don't know how to write it out when job is done.
I have an Register-ObjectEvent that fires when each USB is completed in formatting and transfering.
The script works really well and writes out the line USB slot = Job...
But I want to have my $driveLetter from inside $formatDrive written out in the line USB slot = $driveLetter.
Heres my code. Look for the lines $formatDrive and $jobEvent.
#Requires -version 2.0
ipmo storage
Register-WmiEvent -Class win32_VolumeChangeEvent -SourceIdentifier volumeChange
$formatDrive = {
Param($driveLetter)
Write-Host (Get-Date -Format s) "Erase disk..."
$source = "C:\Users\myname\Desktop\test"
Format-Volume -DriveLetter $driveLetter[0] -NewFileSystemLabel "test30" -FileSystem exFAT -Confirm:$false
robocopy $source $driveLetter /S
return $driveLetter
}
Write-Host (Get-Date -Format s) " Beginning script..."
do {
$newEvent = Wait-Event -SourceIdentifier volumeChange
$eventType = $newEvent.SourceEventArgs.NewEvent.EventType
$eventTypeName = switch ($eventType) {
1 {"Configuration changed"}
2 {"Device arrival"}
3 {"Device removal"}
4 {"docking"}
}
if ($eventType -eq 2) {
$driveLetter = $newEvent.SourceEventArgs.NewEvent.DriveName
$driveLabel = ([wmi]"Win32_LogicalDisk='$driveLetter'").VolumeName
Write-Host (Get-Date -Format s) "USB igang = " $driveLetter
# Execute process if drive matches specified condition(s)
$formatDrivejob = Start-Job -ScriptBlock $formatDrive -ArgumentList $driveLetter
$jobEvent = Register-ObjectEvent $formatDrivejob StateChanged -Action {
Write-Host (Get-Date -Format s) (' USB slot = Job #{0} ({1}) complete.' -f $sender.Id, $sender.Name)
[media.SystemSounds]::("Hand").Play()
$jobEvent | Unregister-Event
}
}
Remove-Event -SourceIdentifier volumeChange
} while (1-eq1) #Loop until next event
Unregister-Event -SourceIdentifier volumeChange
I've tried just writing it out inside the end of $formatDrive - which didn't work as it's running in background.
Also tried with return as you can see in $formatDrive
TRYOUTS:
#Requires -version 2.0
ipmo storage
Register-WmiEvent -Class win32_VolumeChangeEvent -SourceIdentifier volumeChange
$formatDrive = {
param($driveLetter)
write-host (get-date -format s) "Erase disk..."
$source = "C:\Users\jbh\Desktop\test"
Format-Volume -Driveletter $driveLetter[0] -NewFileSystemLabel "test30" -FileSystem exFAT -Confirm:$false
robocopy $source $driveLetter /S
return $driveLetter
}
write-host (get-date -format s) " Beginning script..."
do{
$newEvent = Wait-Event -SourceIdentifier volumeChange
$eventType = $newEvent.SourceEventArgs.NewEvent.EventType
$eventTypeName = switch($eventType)
{
1 {"Configuration changed"}
2 {"Device arrival"}
3 {"Device removal"}
4 {"docking"}
}
# initialize the array
$formatDrivejob = #()
if ($eventType -eq 2) {
$driveLetter = $newEvent.SourceEventArgs.NewEvent.DriveName
$driveLabel = ([wmi]"Win32_LogicalDisk='$driveLetter'").VolumeName
Write-Host (Get-Date -Format s) "USB igang = " $driveLetter
# Execute process if drive matches specified condition(s)
$formatDrivejob += Start-Job -ScriptBlock $formatDrive -ArgumentList $driveLetter
$jobEvent = Register-ObjectEvent $formatDrivejob StateChanged -Action {
Write-Host (Get-Date -Format s) (' USB slot = Job #{0} ({1}) complete.' -f $sender.Id, $sender.Name)
[media.SystemSounds]::("Hand").Play()
foreach ($i in $formatDrivejob){
if ($i.State -eq "Completed")
{
$letter = $formatDrivejob | receive-job | select -Last 1
Write-Host "Drive has finished:" $letter
$formatDrivejob.Remove($i)
}
}
$jobEvent | Unregister-Event
}
}
Remove-Event -SourceIdentifier volumeChange
} while (1-eq1) #Loop until next event
Unregister-Event -SourceIdentifier volumeChange
You can use Receive-job to get the results:
# initialize the array
[System.Collections.ArrayList]$formatDrivejob
if ($eventType -eq 2) {
$driveLetter = $newEvent.SourceEventArgs.NewEvent.DriveName
$driveLabel = ([wmi]"Win32_LogicalDisk='$driveLetter'").VolumeName
Write-Host (Get-Date -Format s) "USB igang = " $driveLetter
# Execute process if drive matches specified condition(s)
$formatDrivejob += Start-Job -ScriptBlock $formatDrive -ArgumentList $driveLetter
$jobEvent = Register-ObjectEvent $formatDrivejob StateChanged -Action {
Write-Host (Get-Date -Format s) (' USB slot = Job #{0} ({1}) complete.' -f $sender.Id, $sender.Name)
[media.SystemSounds]::("Hand").Play()
foreach ($i in $formatDrivejob){
if ($i.State -eq "Completed")
{
$letter = $formatDrivejob | receive-job | select -Last 1
Write-Host "Drive has finished:" $letter
$formatDrivejob.Remove($i)
}
}
$jobEvent | Unregister-Event
}
}
Maybe you can try an approach like this:
$DrivesToHandle = 5
$formatDrivejob = #()
Do {
if ($eventType -eq 2) {
$DrivesToHandle -= 1
$driveLetter = $newEvent.SourceEventArgs.NewEvent.DriveName
$driveLabel = ([wmi]"Win32_LogicalDisk='$driveLetter'").VolumeName
write-host (Get-Date -format s) "USB igang = $driveLetter"
# Execute process if drive matches specified condition(s)
$formatDrivejob += Start-Job -ScriptBlock $formatDrive -ArgumentList $driveLetter
}
} while ($DrivesToHandle -ne 0)
# Wait for all jobs to finish
$formatDrivejob | Wait-Job | Out-Null
# Retrieve the job results
$JobResults = $formatDrivejob | Receive-Job
foreach ($Job in $JobResults) {
Write-Host (get-date -format s) "Job result of job ID $($Job.Id): " $Job
}
# Remove all jobs
$formatDrivejob | Remove-Job -Force
When you work with jobs you can use Wait-Job to wait for the job to finish. But you also have to retrieve the job results, that's done by using the Receive-Job CmdLet.
Related
I have a script that backs up a user profile from the local, or remote machine and places it onto a share: $Global:Shared_BackupPath = "\\server\share\". I've been tweaking it a little more and just ended up making some variables into Global variables (not sure if this is the issue - dont see why it would be).
This is the condition:
if(-not (Get-EventSubscriber)){
I tried changing it to -eq $null to see if it would make any difference, but it didn't.
Its just not analyzing the condition properly and goes on to display my message box before all Jobs are done: it's "supposed" to wait till there's no more events and then display the message box:
Register-ObjectEvent -InputObject $job -EventName StateChanged -Action {
#Start-Sleep -Milliseconds 500
$eventSubscriber | Unregister-Event
$eventSubscriber.Action | Remove-Job
if(-not (Get-EventSubscriber)){
$Profile_Sum = Get-ChildItem -Path $Global:BackUp_Path -Recurse |
Measure-Object -Property length -Sum |
Select-Object -ExpandProperty Sum
$Size = try{if($Profile_Sum -lt 1048576){ $TinyByte = " {0:N2}" -f ($Profile_Sum / 1KB) + " KB"; $TinyByte }
elseif($Profile_Sum -gt 1048576 -and $Profile_Sum -lt 1073741824){ $MediumByte = " {0:N2}" -f ($Profile_Sum / 1MB) + " MB"; $MediumByte }
elseif($Profile_Sum -gt 1073741824){ $GiganticByte = " {0:N2}" -f ($Profile_Sum / 1GB) + " GB"; $GiganticByte } } Catch {}
$Begin_Time = Get-Item -Path $Global:BackUp_Path | Select-Object -ExpandProperty LastWriteTime
$End_Time = Get-Date -Format G
Get-Job | Remove-Job
[System.Windows.MessageBox]::Show("Copying Complete!`nStart Time: $Begin_Time `nEnd Time: $End_Time `nProfile Size copied: $Size")
}
} | Out-Null
}
I feel like I may have an idea due to the event itself being registered as a job but, not too sure how to go about it and have it wait until its done with ALL jobs before displaying the messagebox informing me when the copying is complete. Other than that, the script works just fine and anyone reading this can feel free to use it themselves. Heres the full Script:
Function PFL-UserBackUp{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$false,
ValueFromPipeLine=$true,
ValueFromPipeLineByPropertyName=$true)]
[Alias('CN','Computer','server','node')]
[ValidateLength(1, 15)]
[String[]]$ComputerName = $env:COMPUTERNAME )
Begin{
$Global:Shared_BackupPath = "\\server\share\"
}
Process{
Foreach($Computer in $ComputerName){
Try{
$PSSession = New-PSSession -ComputerName $Computer -ErrorAction Stop
[array]$User_List = Invoke-Command -ScriptBlock {
Get-ChildItem -Path "C:\Users" -Exclude Public, Default* |
Sort-Object -Property LastWriteTime -Descending } -Session $PSSession
$userinfo1 = foreach ($user in $User_List.name) {
$userinfo = (net user $user /domain | Select-String "Full Name" -ErrorAction SilentlyContinue) -replace "Full Name ", "" 2>&1 | Out-String -Stream
if ($userinfo.Length -lt 4) { "NO DISPLAY NAME in ADUC" }
elseif($LASTEXITCODE -eq 2) { "ACCOUNT NOT in ADUC" }
elseif($LASTEXITCODE -eq 0) { $userinfo }
else { "Error occured" }
}
$(for($i=0; $i -lt $User_List.Count; $i++){
[pscustomobject]#{
'User Display Name ' = "$($i): $($userinfo1[$i])"
' NAME ' = $User_List.name[$i]
'Last Modified' = "$($User_List.LastWriteTime[$i])"
'Profile Size ' = Try{
$ProfilePath = $User_List.FullName[$i]
$Profile_Sum = Invoke-Command -ScriptBlock {
Get-ChildItem -Path $Using:ProfilePath -Recurse |
Where-Object {$_.PSParentPath -match "Documents|Desktop|Music|Videos|Downloads|Links|Pictures|Favorites|Contacts" -and $_.DirectoryName -notmatch "OneDrive" } |
Measure-Object -Property length -Sum |
Select-Object -ExpandProperty Sum } -Session $PSSession
if($Profile_Sum -lt 1048576){ $TinyByte = " {0:N2}" -f ($Profile_Sum / 1KB) + " KB"; $TinyByte }
elseif($Profile_Sum -gt 1048576 -and $Profile_Sum -lt 1073741824){ $MediumByte = " {0:N2}" -f ($Profile_Sum / 1MB) + " MB"; $MediumByte }
elseif($Profile_Sum -gt 1073741824){ $GiganticByte = " {0:N2}" -f ($Profile_Sum / 1GB) + " GB"; $GiganticByte } #Profile Size
} Catch { "$($Error[0].Exception.Message.Split('.')[2].Trim())!" }
}
} ) | Out-Host
Write-Host "Press 'Q' to quit."
$ii = Read-Host -Prompt "Enter Number of Profile to Back-Up"
$i = $ii.Trim() -split ","
if([String]::IsNullOrEmpty($i) -eq $true) { "Null string"; Break }
elseif($i.ToLower() -like "q*") {"Q was selected. Stopping script."; Break }
<#
" "
" Following Profiles will be Saved:"
" ------------------------------------"
foreach($i in $index) { "$($i.trim()): $($userinfo1[$i])" }
" "
$Confirm = Read-Host -Prompt "Are you sure you want to continue? [Y/N]"
if($Confirm.ToLower().TrimStart() -like "n*" -or $Confirm.ToLower() -like "q*"){Break}
if([String]::IsNullOrEmpty($Confirm.Trim()) -eq $true) { "Null string"; Break }#>
$Profile_Path = "C:\Users\$($User_List.name[$i])"
$Literal_Name = $userinfo1[$i].Replace('/','-')
$Global:BackUp_Path = "$Global:Shared_BackupPath$Literal_Name"
$Test_Path = Test-Path -Path $Global:BackUp_Path
if($Test_Path -eq $false){
New-Item -Path $Global:BackUp_Path -ItemType Directory | Out-Null
Start-Process $Global:BackUp_Path}
elseif($Test_Path -eq $true){
$Find_OldName = Get-ChildItem -Path "$Global:Shared_BackupPath" -Filter "$Literal_Name" -Directory |
Sort-Object -Property LastWriteTime -Descending |
Select-Object -ExpandProperty BaseName -First 1
$New_PathName = $Find_OldName + "1"
New-Item -Path "$Global:Shared_BackupPath" -Name $New_PathName -ItemType Directory -OutVariable Global:BackUp_Path | Out-Null #Global:BackUp_Path variable declared
$Global:BackUp_Path = $Global:BackUp_Path.FullName
Start-Process $Global:BackUp_Path}
$Global:Start_Time = Get-Date -Format G
#Favorites Copy
$FireFox_Favorites = "C:\Users\$($User_List.name[$i])\AppData\Roaming\Mozilla\Firefox\Profiles\*.default\places.sqlite"
$Chrome_Favorites = "C:\Users\$($User_List.name[$i])\AppData\Local\Google\Chrome\User Data\Default\Bookmarks"
$Chrome_Favorites2 = "C:\Users\$($User_List.name[$i])\AppData\Local\Google\Chrome\User Data\Default\Bookmarks.bak"
$Sticky_Notes = "C:\Users\$($User_List.name[$i])\AppData\Local\Packages\Microsoft.MicrosoftStickyNotes_8wekyb3d8bbwe\LocalState\plum.sqlite"
$Favorites_Array = #($FireFox_Favorites,$Chrome_Favorites,$Chrome_Favorites2,$Sticky_Notes)
Foreach($File in $Favorites_Array){
$Test_File = Invoke-Command -ScriptBlock { Test-Path -Path $File }
if($Test_File -eq $true){
Copy-Item -Path $File -Destination $Global:BackUp_Path -Force -Recurse -FromSession $PSSession
}
}
#Folders Copy
$Folders = #('Desktop','Documents','Favorites','Links','Downloads','Music','Videos','Pictures','Contacts')
Foreach($Folder in $Folders){
#Create Arugments for seperate thread
$ArgumentsArray = $null
$ArgumentsArray = #()
$ArgumentsArray += "\\$Computer\c$\Users\$($User_List.name[$i])\$Folder"
$ArgumentsArray += $Global:BackUp_Path
$job = Start-Job -ScriptBlock { Copy-Item -Path $args[0] -Destination $args[1] -Force -Recurse } -Name $Folder -ArgumentList $ArgumentsArray
Register-ObjectEvent -InputObject $job -EventName StateChanged -Action {
#Start-Sleep -Milliseconds 500
$eventSubscriber | Unregister-Event
$eventSubscriber.Action | Remove-Job
if(-not (Get-EventSubscriber)){
$Profile_Sum = Get-ChildItem -Path $Global:BackUp_Path -Recurse |
Measure-Object -Property length -Sum |
Select-Object -ExpandProperty Sum
$Size = try{if($Profile_Sum -lt 1048576){ $TinyByte = " {0:N2}" -f ($Profile_Sum / 1KB) + " KB"; $TinyByte }
elseif($Profile_Sum -gt 1048576 -and $Profile_Sum -lt 1073741824){ $MediumByte = " {0:N2}" -f ($Profile_Sum / 1MB) + " MB"; $MediumByte }
elseif($Profile_Sum -gt 1073741824){ $GiganticByte = " {0:N2}" -f ($Profile_Sum / 1GB) + " GB"; $GiganticByte } } Catch {}
$Begin_Time = Get-Item -Path $Global:BackUp_Path | Select-Object -ExpandProperty LastWriteTime
$End_Time = Get-Date -Format G
Get-Job | Remove-Job
[System.Windows.MessageBox]::Show("Copying Complete!`nStart Time: $Begin_Time `nEnd Time: $End_Time `nProfile Size copied: $Size")
}
} | Out-Null
}
" "
Write-Output -InputObject "Copying will be done in background."
Write-Output -InputObject "You will be notified when copying is done."
} catch [System.Management.Automation.Remoting.PSRemotingTransportException]{
"Unable to connect to PC: $Computer `nError: $($Error[0].Exception.Message.Split('.')[2].Trim())!"
}
}
}
}
I removed some information that could get me in trouble but, it's all cosmetics.(:
EDIT: I must be on crack but, everything is working now. Only changes I made was clearing the global variables ($Global:var = $null) before assigning it a value. Thank you all for the suggestions. Another change i made was change Copy-Item to Robocopy instead.
To just answer your question:
Why is my if statement not being read properly?
Just before you evaluate Get-EventSubscriber your are cancelling your event subscription and thus deleting your event subscriber. Get-EventSubscriber therefore returns $null which evaluates to $true, when negated. In conclusion, the code after your if statement will always be executed immediately.
Commented code:
Register-ObjectEvent -InputObject $job -EventName StateChanged -Action {
$eventSubscriber | Unregister-Event # Here you cancel your event subscription
$eventSubscriber.Action | Remove-Job
if (-not (Get-EventSubscriber)) { # Therefore, Get-EventSubscriber returns $null; not $null evaluates to $true
...
The relevant part in the documentation of Unregister-Event:
The Unregister-Event cmdlet cancels an event subscription that was created by using the Register-EngineEvent, Register-ObjectEvent, or Register-WmiEvent cmdlet.
When an event subscription is canceled, the event subscriber is deleted from the session and the subscribed events are no longer added to the event queue. When you cancel a subscription to an event created by using the New-Event cmdlet, the new event is also deleted from the session.
I'm using PowerShell to start a bat file that wipes a USB drive that connected.
If I use the script without Start-Process it works fine, but I'm wanting to connect multiple drives and have it wipe them simultaneously.
The script:
Register-WmiEvent -Class Win32_VolumeChangeEvent -SourceIdentifier VolumeChange
Write-Host (Get-Date -Format s) " Beginning script..."
do {
$newEvent = Wait-Event -SourceIdentifier volumeChange
$eventType = $newEvent.SourceEventArgs.NewEvent.EventType
$eventTypeName = switch ($eventType) {
1 {"Configuration changed"}
2 {"Device arrival"}
3 {"Device removal"}
4 {"docking"}
}
Write-Host (Get-Date -Format s) " Event detected = " $eventTypeName
if ($eventType -eq 2) {
$driveLetter = $newEvent.SourceEventArgs.NewEvent.DriveName
$driveLabel = ([wmi]"Win32_LogicalDisk='$driveLetter'").VolumeName
Write-Host (Get-Date -Format s) " Drive name = " $driveLetter
Write-Host (Get-Date -Format s) " Drive label = " $driveLabel
# Execute process if drive matches specified condition(s)
if ($driveLabel -eq 'BBIFREE_01' -or $drivelabel -eq 'HD10') {
Write-Host (Get-Date -Format s) " Starting task in 3 seconds..."
Start-Sleep -Seconds 3
Start-Process -FilePath D:\wipe.bat $driveLetter, $driveLabel
Copy-Item -Path D:\Utilities1 -Destination $driveLetter -Recurse
$driveEject = New-Object -ComObject Shell.Application
$driveEject.Namespace(17).ParseName($driveLetter).InvokeVerb("Eject")
}
}
Remove-Event -SourceIdentifier VolumeChange
} while (1 -eq 1) #Loop until next event
Unregister-Event -SourceIdentifier VolumeChange
The bat file contents:
set arg1=%1
set arg2=%2
format %args1% /FS:NTFS /p:1 /V:%args2% /x /y
EDIT
To clarify: the script is to run continously on a specific PC where it should start the bat file (as in wipe the disk securely) every time it detects a disk being connected.
If I use:
D:\wipe.bat -ArgumentList `"$driveLetter",`"$driveLabel"
then it starts the wiping on 1 disk, and on 1 disk only.
I need it to detect multiple disks, that's why I used Start-Process, seeing as I thought it would run on the background and keep watching for new events.
EDIT2
I changed the code to avoid using -ArgumentList, see above.
If I put the echo command in my batch file as requested:
set arg1=E:
set arg2=BBIFREE_01
ECHO ECHO IS ON
ECHO ECHO IS ON
So I see the commands in the bat file, but it doesn't execute and goes straight for the copy command.
This is a slightly modified version of a Script I wrote a while back, I don't have time right now to confirm it works 100% but it should at least point you in the right direction, it just threads the actual wiping so it can handle other jobs in the background, then uses a global popup to warn when one is done to prevent having to block while the job is finishing.
Should be able to handle any number of devices at once, it uses PowerShell's Format-Volume command instead, but you could put a call to the BAT file inside the job instead.
$USBWhiteList = #( #Add wildcard items here, if a USB matches one it will be wiped.
"USB0*"
"*WIPE"
)
Enum EventNames{ Changed = 1 ; Inserted = 2 ; Removed = 3 ; Docking = 4 } #Names for events
Register-WmiEvent -Class win32_VolumeChangeEvent -SourceIdentifier volumeChange -ErrorAction SilentlyContinue #register the event
do{
Write-Host "Monitoring for Disk events..." -Fore Yellow
$Event = Wait-Event -SourceIdentifier volumeChange #wait for a disk event
$EventType = [EventNames]$Event.SourceEventArgs.NewEvent.EventType #get the type of the event
Write-Host "Drive $($EventType), Processing..." -Fore Yellow -NoNewline
$Volume = Get-Volume -DriveLetter $Event.SourceEventArgs.NewEvent.DriveName -ErrorAction SilentlyContinue #get the volume details
$IsMatch = ($USBWhiteList|? {$Volume.FileSystemLabel -like $_}).Count -gt 0 #does it match our whitelist?
if (($EventType -eq [EventNames]::Inserted) -and $IsMatch){ #if a disk was inserted which matches the whitelist...
Write-Host "Volume $($Volume.DriveLetter): '$($Volume.FileSystemLabel)', Found, Wiping!" -Fore Green
Start-Job -ScriptBlock { param ($Volume) #Perform the wipe inside a job
$Disk = Get-Partition -DriveLetter $Volume.DriveLetter | Get-Disk
Clear-Disk -Number $Disk.Number -RemoveData -Confirm:$false
New-Partition -DiskNumber $Disk.Number -UseMaximumSize -IsActive -DriveLetter $Volume.DriveLetter
Format-Volume -FileSystem NTFS -DriveLetter $Volume.DriveLetter -Confirm:$false
Add-Type -AssemblyName 'System.Windows.Forms' #warn (globally) when it is finished, don't need to run wait/recieve job.
[System.Windows.Forms.MessageBox]::Show("Finished Wiping Disk $($Volume.DriveLetter)","Please Remove Disk")
} -ArgumentList $Volume | Out-Null
} else {
Write-Host "Ignoring" -Fore Red
}
Remove-Event -SourceIdentifier volumeChange
} while (1) #this should be modified to quit after x disks or something, the below commands won't get exec'd - could also use a Try/Finally and Ctrl+C the script.
Get-Job | Remove-Job -Force
Unregister-Event -SourceIdentifier volumeChange
I have inherited a script that is not working. I need to capture everything that would normally output to the console, including Success and Error entries from the script. This is only a small portion of the script, and it only captures errors. Any help would be appreciated on getting all output to the file instead of the console.
An example is the Write-Verbose "VERIFYING contact for $($User.WindowsEmailAddress)"
I know this is writing to the console, but I need it to write to the log that is defined at the very bottom of the script.
Catch
{Out-File -InputObject "$(Get-Date -Format MM.dd.yyyy-HH:mm:ss);$($WriteMode);ERROR;Target;$($targetUser.Split('#')[1]);$($User.WindowsEmailAddress);Update;;;Error updating user: $($Error[0])" -FilePath $LogFilePath -Append}
I hope this makes sense.
### UPDATES
ForEach ($User in $colUpdContact)
{
Write-Verbose "VERIFYING contact for $($User.WindowsEmailAddress)"
#Filter used to find the target contact object(s)
$strFilter = "WindowsEmailAddress -eq `"$($User.WindowsEmailAddress)`""
Try
{$colContacts2 = Invoke-Command -Session $targetSession -ScriptBlock {param ($strFilter) Get-Contact -Filter $strFilter} -ArgumentList $strFilter -ErrorAction Stop}
Catch
{Out-File -InputObject "$(Get-Date -Format MM.dd.yyyy-HH:mm:ss);$($WriteMode);ERROR;Target;$($targetUser.Split('#')[1]);$($User.WindowsEmailAddress);Find;;;Error getting contact: $($Error[0])" -FilePath $LogFilePath -Append}
ForEach ($Contact in $colContacts2)
{
#initialize update string and cmd string
$strUpdateContact = $null
$updateCmd = $null
$strWriteBack = $null
$writeBackCmd = $null
#Iterate through attributes and append to the strUpdateContact string if the attribute value has changed
ForEach ($Attrib in $arrAttribs)
{
If ($User.$Attrib -ne $Contact.$Attrib)
{
if($ReadOnly){
Add-Content -Path $readOnlyFilePath -Value " Changing $Attrib"
Add-Content -Path $readOnlyFilePath -Value " Before: $($Contact.$Attrib)"
Add-Content -Path $readOnlyFilePath -Value " After: $($User.$Attrib)"
}
$strUpdateContact += " -$($Attrib) `"$($User.$Attrib)`""
Out-File -InputObject "$(Get-Date -Format MM.dd.yyyy-HH:mm:ss);$($WriteMode);CHANGE;Target;$($targetUser.Split('#')[1]);$($User.WindowsEmailAddress);Update;$($Contact.$Attrib);$($User.$Attrib);" -FilePath $LogFilePath -Append
}
}
#Check if LegacyExchangeDN has been written back to User object
$mailContact = Invoke-Command -Session $targetSession -ScriptBlock {param ($contact) Get-MailContact $($contact.WindowsEmailAddress)} -ArgumentList $Contact -ErrorAction Stop
$x500 = "X500:$($mailContact.LegacyExchangeDN)"
$userRec = Invoke-Command -Session $sourceSession -ScriptBlock {param ($User) Get-Recipient $($User.WindowsEmailAddress)} -ArgumentList $User -ErrorAction Stop
if($UserRec.emailAddresses -notcontains $x500){
$userName = ($user.UserPrincipalName).Split('#')[0]
if($userName -eq "")
{
$userName = $user.SamAccountName
}
$strWriteBack = "Set-ADUser -Identity $userName -Add #{ProxyAddresses=`"$x500`"} -Server $sourceDC -Credential `$sourceDCCredential"
}
#If there is anything to update
If ($strUpdateContact.Length -gt 0)
{
Write-Verbose "Updating attributes for $($User.WindowsEmailAddress)"
#Prepend the command for the contact being modified
$strUpdateContact = "Set-Contact $($User.WindowsEmailAddress) " + $strUpdateContact
If ($ReadOnly)
{Add-Content -Path $readOnlyFilePath -Value $strUpdateContact}
Else
{
Try
{
#Create the complete command and invoke it
$updateCmd = "Invoke-Command -Session `$targetSession -ScriptBlock {$($strUpdateContact)}"
Invoke-Expression $updateCmd -ErrorAction Stop
}
Catch
{Out-File -InputObject "$(Get-Date -Format MM.dd.yyyy-HH:mm:ss);$($WriteMode);ERROR;Target;$($targetUser.Split('#')[1]);$($User.WindowsEmailAddress);Update;;;Error updating contact: $($Error[0])" -FilePath $LogFilePath -Append}
}
}
If ($strWriteBack){
Write-Verbose "Updating X500 for $($User.WindowsEmailAddress)"
Out-File -InputObject "$(Get-Date -Format MM.dd.yyyy-HH:mm:ss);$($WriteMode);CHANGE;Target;$($targetUser.Split('#')[1]);$($User.WindowsEmailAddress);Update;;$x500;" -FilePath $LogFilePath -Append
If($ReadOnly){
Add-Content -Path $readOnlyFilePath -Value $strWriteBack
}
else{
Try
{
Invoke-Expression $strWriteBack -ErrorAction Stop
}
Catch
{Out-File -InputObject "$(Get-Date -Format MM.dd.yyyy-HH:mm:ss);$($WriteMode);ERROR;Target;$($targetUser.Split('#')[1]);$($User.WindowsEmailAddress);Update;;;Error updating user: $($Error[0])" -FilePath $LogFilePath -Append}
}
}
}
}
Why you not use the Start-Transcript to output all the information into a log file, and then you can manually copy anything you want?
An example for the command:
Start-Transcript -Path $TranscriptOutputFile -Append -Force
#Your script; write-output 'something update';
Stop-Transcript
Everything output by write-output command will be appended into the log file.
I made a script to check if users desktop folder are under the cuota limitation, if they're under the cuota limitation the backup to the server will be done correctly.
each user have his computer, so source CSV looks like:
pc1,user1
pc2,user2
pc800,user800
Some computers are Windows Xp and some W7, and the paths can be different 'cause of that I'm using Test-Path
W7 = C:\users\$user\desktop
XP = C:\document and settings\$user\desktop
But Test-Path is SUPER SLOW and I started to use a Test-Connection -count 1 before each Test-path
Anyway, the script still SLOW, in each "bad ping test" I lose lot of time.
CODE:
$csvLocation = '~\desktop\soourceReport.csv'
$csv = import-csv $csvLocation -Header PCName, User
$OuputReport = '~\desktop\newReport.csv'
# info:
# "209715200" Bytes = 200 MB
$cuota = "209715200"
$cuotaTranslate = "$($cuota / 1MB) MB"
Write-Host "Cuota is set to $cuotaTranslate"
$count=1
foreach($item in $csv)
{
write-host "$count# Revisando" $item.User "en" $item.PCName "..." #For debug
if (Test-Connection -Quiet -count 1 -computer $($item.PCname)){
$w7path = "\\$($item.PCname)\c$\users\$($item.User)\desktop"
#echo $w7path #debug
$xpPath = "\\$($item.PCname)\c$\Documents and Settings\$($item.User)\Escritorio"
#echo $xp #debug
if(Test-Path $W7path){
$desktopSize = (Get-ChildItem -Recurse -force $w7path | Measure-Object -ErrorAction "SilentlyContinue" -property length -sum)
write-host -ForegroundColor Green "access succeed"
if($($desktopSize.sum) -gt $cuota){
$newLine = "{0},{1},{2}" -f $($item.PCname),$($item.User),"$("{0:N0}" -f $($desktopSize.sum / 1MB)) MB"
$newLine | add-content $outputReport
Write-Host -ForegroundColor Yellow "cuota exceeded! -- added"
}
else{
Write-Host -ForegroundColor DarkYellow "cuota OK"
}
}
elseif(Test-Path $xpPath){
$desktopSize = (Get-ChildItem -Recurse -force $xpPath | Measure-Object -ErrorAction "SilentlyContinue" -property length -sum)
write-host -ForegroundColor Green "access succeed"
if($($desktopSize.sum) -gt $cuota){
$newLine = "{0},{1},{2}" -f $($item.PCname),$($item.User),"$("{0:N0}" -f $($desktopSize.sum / 1MB)) MB"
$newLine | add-content $outputReport
Write-Host -ForegroundColor Yellow "cuota exceeded! -- added"
}
else{
Write-Host -ForegroundColor DarkYellow "cuota OK"
}
else{
write-host -ForegroundColor Red "Error! - bad path"
}
}
else{
write-host -ForegroundColor Red "Error! - no ping"
}
$count++
}
Write-Host -ForegroundColor green -BackgroundColor DarkGray "All done! new report stored in $report"
To improve it I stored all computers in a $list using another Foreach, before the firstly mentioned SLOW-Foreach loop.
foreach($pcs in $csv){
$alivelist += #( $pcs.PCName )
}
Test-Connection -quiet -count 2 -computer $alivelist
Now, I don't now how to UPDATE or remove the rows ("dead" pc,user) from the SOURCE CSV before to enter into the second Foreach.
I need some of your "magic", or at least some ideas!
thanks
To speed up your script you need to run the checks in parallel (as others have already mentioned). Put your checks and the worker code in a scriptblock:
$sb = {
Param($computer, $username)
if (Test-Connection -Quiet -Count 2 $computer) { return }
$w7path = "\\$computer\c$\users\$username\desktop"
$xpPath = "\\$computer\c$\Documents and Settings\$username.TUITRA..."
if (Test-Path $W7path) {
#...
} elseif (Test-Path $xpPath) {
#...
} else {
#...
}
}
Then run the scriptblock as parallel jobs:
$csv | ForEach-Object {
Start-Job -ScriptBlock $sb -ArgumentList $_.PCName, $_.User
}
# wait for completion
do {
Start-Sleep -Milliseconds 100
} while (Get-Job -State 'Running')
# cleanup
Get-Job | ForEach-Object {
Receive-Job -Id $_.Id
Remove-Job -Id $_.Id
} | Out-File $outputReport
Use a queue if you need to limit the number of parallel jobs.
test-connection is weirdly fast with the -asjob parameter, pinging about 200 computers in 4 seconds:
$list = cat hp.txt
test-connection $list -AsJob ; job | receive-job -wait -AutoRemoveJob
I have pieced together the following PowerShell script that deletes a file from the public desktop of every machine in an OU on our domain, and then copies a replacement file back to replace it. It works well, except for the machines that are offline. What would be the best way to have the script run on a machine once it comes online? My best guess is to have it put any offline machine in a list, then re-run a few hours later for the computers on that list. Is there a better way?
$DESTINATION = "c$\Users\Public\Desktop"
$REMOVE = "ComputerName"
$strFilter = "computer"
$objDomain = New-Object System.DirectoryServices.DirectoryEntry
$objSearcher = New-Object System.DirectoryServices.DirectorySearcher
$objSearcher.SearchRoot = "LDAP://OU=MyOU,DC=Domain,DC=com"
$objSearcher.SearchScope = "Subtree"
$objSearcher.PageSize = 1000
$objSearcher.Filter = "(objectCategory=$strFilter)"
$colResults = $objSearcher.FindAll()
foreach ($i in $colResults)
{
$objComputer = $i.GetDirectoryEntry()
$REMOVE = $objComputer.Name
#Ping system to see if it's on
$rtn = Test-Connection -CN $REMOVE -Count 2 -BufferSize 16 -Quiet
IF($rtn -match 'True') {
Remove-Item "\\$REMOVE\$DESTINATION\SparksNET.url" -Recurse
Copy-Item "\\spd3\PD IT stuff\Software\Desktop Icons\mySparks.website" "\\$REMOVE\$DESTINATION"
}
ELSE {
}
}
Everyone is 100% correct, you should do this through GPO. However, if you crazily want to do it through PowerShell, you could do it the way I have outlined below. I threw your code into it - so it might be a tad messy.
Start off by pulling in the list of PC's and then send them all off to a job...
if(Test-Path "c:\LogPath"){
}else {
mkdir "c:\LogPath"
}
$time=Get-Date -Format s
$date=Get-Date -Format F
"LOG FILE CREATED - $date" | Add-Content $log
$objDomain = New-Object System.DirectoryServices.DirectoryEntry
$objSearcher = New-Object System.DirectoryServices.DirectorySearcher
$objSearcher.SearchRoot = "LDAP://OU=MyOU,DC=Domain,DC=com"
$objSearcher.SearchScope = "Subtree"
$objSearcher.PageSize = 1000
$objSearcher.Filter = "(objectCategory=$strFilter)"
$colResults = $objSearcher.FindAll()
foreach($Obj in $colResults){
$objComputer = $Obj.GetDirectoryEntry()
$remotePC = $objComputer.Name
#Imports all functions used in script
. "C:\PathToYourJobScript.ps1"
#Creating Jobs
$jobs = (get-job -state running | Measure-Object).count
Get-job -State Completed | Remove-Job
while($jobs -ge 5)
{
#get currently running jobs after restart
get-job -state running | %{write-host $_.PSBeginTime} | ?{($_.PSBeginTime - $(Get-Date)).Minutes -gt 10} | Remove-Job
Write-host "Currently running maximum threads at: $jobs `n Sleeping 5 seconds" -ForegroundColor DarkYellow
start-sleep -seconds 5
$jobs = (get-job -state running | Measure-Object).count
}
Write-host "`t`tCreating Thread for $remotePC" -ForegroundColor Yellow
start-job $DeleteFile -ArgumentList $remotePC -Name $remotePC
Receive-Job $remotePC
"$remotePC; Starting Job at; $time" | Add-Content $log
}
In your actual job script PS1, wrap your code above into one function
$DeleteFile={Param(
[Parameter(Mandatory=$true)]
[string]$remotePC
)
<#
.SYNOPSIS
.DESCRIPTION
.NOTES
#>
##Globals
$DESTINATION = "c$\Users\Public\Desktop"
$REMOVE = "ComputerName"
$strFilter = "computer"
#Ping system to see if it's on
if(Test-Connection -ComputerName $remotePC -Count 2 -BufferSize 16 -ErrorAction SilentlyContinue){
Remove-Item "\\$REMOVE\$DESTINATION\SparksNET.url" -Recurse
Copy-Item "\\spd3\PD IT stuff\Software\Desktop Icons\mySparks.website" "\\$REMOVE\$DESTINATION"
"$remotePC; Removal Complete; $time" | Add-Content $log
}
ELSE
{
do {Start-Sleep -Seconds 300; "$remotePC; Ping Fail; $time" | Add-Content $log}
until (Test-Connection -ComputerName $remotePC -Count 2 -BufferSize 16 -ErrorAction SilentlyContinue)
Remove-Item "\\$REMOVE\$DESTINATION\SparksNET.url" -Recurse
Copy-Item "\\spd3\PD IT stuff\Software\Desktop Icons\mySparks.website" "\\$REMOVE\$DESTINATION"
"$remotePC; Removal Complete; $time" | Add-Content $log
}
}
This will check every 5 minutes if the PC is online and wait until it gets a response to proceed. Once an item is tossed into a job, you loose site of its progress and would want to log accordingly so you know its position.