Windows duplicating printer with custom name - windows

My organization uses a piece of software that has a printer name hard-coded into it PRN1.
Users are spread out through multiple locations; so it is impossible to just rename a single printer. Our networked printers are mapped by computer location via a login script; and some computers have locally attached printers.
The current proposed solution is to remote into each computer with the user logged in; re-map the users default printer; and manually rename it (Right click->Printer Prop...).
I'm trying to avoid this as we do not use roaming profiles and some users do move from location to location; and the users wouldn't understand why it suddenly isn't working.
Ideally I'd like to create a script that will automatically duplicate the users default printer; and name it PRN1.
$AllPrinters = gwmi win32_printer
$DefaultPrinter = $AllPrinters | where {$_.Default -eq $true}
rundll32 printui.dll,PrintUIEntry /ga /n $DefaultPrinter.SystemName + "\" + $DefaultPrinter.ShareName /z /b"PRN1"
Above is what I currently have; I know I'm not using the rundll32 command properly as the printer isn't being duplicated with the new name.
Any help or assistance would be greatly appreciated.

I use that rundll32 command here and there but never have solid luck with it.
You could do something like this by using wmi entirely. You may want to wrap a try catch around a large chunk of this to suppress errors and log output if users do end up having issues.
$Name = "PRN"
$AllPrinters = gwmi win32_printer
$DefaultPrinter = $AllPrinters | where {$_.Default -eq $true}
$objHelper = [WMICLASS]"\\localhost\root\cimv2:Win32_SecurityDescriptorHelper"
$print = [WMICLASS]"\\localhost\root\cimv2:Win32_Printer"
$print.Scope.Options.EnablePrivileges = $true
$newprinter = $print.createInstance()
$newprinter.drivername = $DefaultPrinter.DriverName
$newprinter.PortName = $DefaultPrinter.PortName
$newprinter.Shared = $false
$newprinter.Location = $DefaultPrinter.Location
$newprinter.Comment = $DefaultPrinter.Comment
$newprinter.DeviceID = $Name
$newprinter.PrintProcessor = $DefaultPrinter.PrintProcessor
$newprinter.PrintJobDataType = $DefaultPrinter.DataType
$newprinter.RawOnly = $DefaultPrinter.RawOnly
$result = $newprinter.Put()

Related

WMI CommandLineTemplate Variables Cutting Off

I've been working on a solution to monitor and respond to certain windows services stopping, and I could really use a few hundred extra sets of eyes on this. I'm setting up the WMI subscription in Powershell and the subscription seems to do it's job, but I'm not getting the expected output using the CommandLineTemplate. I'm trying to push the service name, current state, and previous state to a powershell script or executable (same PS script but compiled) but I only get part of the first variable before it cuts off. I've tried formatting the commandlinetemplate multiple different ways, escaping the variables with single/double/escaped quotes, and tried re-ordering the variables, but it always seems to be part of the first and nothing else gets passed. For testing I'm just trying to grab the variables and write them to a log before I move on to the more fun stuff.
Subscription Code:
$instanceFilter = ([wmiclass]"\\.\root\subscription:__EventFilter").CreateInstance()
$instanceFilter.QueryLanguage = "WQL"
$instanceFilter.Query = "select * from __instanceModificationEvent within 5 where targetInstance isa 'win32_Service' AND targetInstance.Name LIKE 'ServiceNamex.%'"
$instanceFilter.Name = "ServiceFilter"
$instanceFilter.EventNamespace = 'root\cimv2'
$result = $instanceFilter.Put()
$newFilter = $result.Path
#Creating a new event consumer
$instanceConsumer = ([wmiclass]"\\.\root\subscription:CommandLineEventConsumer").CreateInstance()
$instanceConsumer.Name = 'ServiceConsumer'
$instanceConsumer.CommandLineTemplate = "C:\Tools\ServiceMonitor.exe `"%TargetInstance.Name%`" `"%TargetInstance.State%`" `"%PreviousInstance.State%`""
$instanceConsumer.ExecutablePath = "C:\Tools\ServiceMonitor.exe"
$result = $instanceConsumer.Put()
$newConsumer = $result.Path
#Bind filter and consumer
$instanceBinding = ([wmiclass]"\\.\root\subscription:__FilterToConsumerBinding").CreateInstance()
$instanceBinding.Filter = $newFilter
$instanceBinding.Consumer = $newConsumer
$result = $instanceBinding.Put()
$newBinding = $result.Path
Target Code (PS1/EXE):
[CmdletBinding()]
param (
[parameter(Position=0)][string]$serviceName = "Error",
[parameter(Position=1)][string]$currentState = "Error",
[parameter(Position=2)][string]$previousState = "Error"
)
Add-Content -path "C:\temp\service.log" -value "$(Get-Date) - The state of $serviceName on $env:Computername has changed from $previousState to $currentState."
If ($currentState -eq "Stopped")
{
Add-Content -path "C:\temp\service.log" -value "$(Get-Date) - Attempting to restart $serviceName."
Start-Service -DisplayName $serviceName
}
Example Output for ServiceNamex.Funct.QA.12 stopping and starting:
10/30/2019 03:52:11 - The state of ServiceNamex.Funct.QA. has changed to Error.
10/30/2019 03:52:16 - The state of ServiceNamex.Funct.QA. has changed to Error.
Chris, It looks like the issue lies in the variables that you're getting from the first script. I'm not sure where you're getting those variables from. The ServiceMonitor.exe application isn't a PS script and I don't see where the subscription calls out to ServiceMonitor.ps1
Just copying your PS lines into a ServiceMonitor.ps1 file, I was able to run the following command and get the subsequent log information.
.\ServiceMonitor.ps1 TestService Running Stopped
11/01/2019 11:01:24 - The state of TestService on 7D25PX1 has changed from Stopped to Running.
11/01/2019 11:13:11 - The state of TestService on 7D25PX1 has changed from Stopped to Running.
11/01/2019 11:13:44 - The state of TestService on 7D25PX1 has changed from Stopped to Running.
Hopefully that helps to at least point you in the right direction. Feel free to respond with more information and I'll be happy to update the post.

How can I set the "Environment" > "Start the following program at logon" property on a local user using powershell?

Background:
I have a server with Windows 2008 R2 installed running as a terminal server session host. I have a long list of local users set-up and configured as remote desktop users. When the users remotely log on using remote desktop connection, a program automatically starts up. When the user closes that program, the session ends. This all works fine if I set it up manually.
My Question:
I have written a script to add a list of local users automatically and setup and configure the properties. The problem is that nowhere can I find how to set the "Environment" > "Start the following program at logon" properties. (See image for the properties I want to set)
A sample portion of my current script is as follow:
$computer = "localhost"
$userName = "aTestUser"
$objComputer = [ADSI]"WinNT://$computer"
$objUser = $objComputer.Create('user', $userName)
$objUser.SetPassword("Password")
$objUser.PSBase.InvokeSet('Description', "Some description for $userName")
$objUser.PSBase.InvokeSet('userflags', 512)
$objUser.PSBase.InvokeSet('passwordExpired', 1)
$objUser.SetInfo();
I also tried this command which doesn't work:
$objUser.PSBase.InvokeSet("TerminalServicesInitialProgram", "C:\programs\a_test_program.exe")
I have searched on Microsoft's MSDN site and Google and StackOverflow but could not find this specific property.
I found a solution here.
$ou = [adsi]"WinNT://127.0.0.1"
$user = $ou.psbase.get_children().find("test")
$user.PSBase.InvokeSet("TerminalServicesInitialProgram", "C:\logoff.bat")
$user.setinfo()
Okay, so I finally got it working. Seems like you have to first create the user then open it again for editing before the InvokeSet sets the TerminalServicesInitialProgram property.
I am not sure, maybe someone can share some experience or explanation.
Thank you to everyone for your help and assistance.
Working Code:
# Read the CSV file and create the users
# The CSV file structure is:
# UserName,FullName,Description
$Users = Import-Csv -Path "C:\Users.csv"
foreach ($User in $Users)
{
# adds user
$computer = "localhost"
$username = $User.UserName
#$username = "atest001"
$fullname = $User.FullName
#$fullname = "My Name"
$description = $User.Description
#$description = "A new user description"
$password = "MyGreatUnbreakableSecretPassword"
$objComputer = [ADSI]"WinNT://$computer"
$objUser = $objComputer.Create('user', $username)
$objUser.SetPassword($password)
$objUser.PSBase.InvokeSet("Description", $description)
$objUser.PSBase.InvokeSet('userflags', 65536)
$objUser.SetInfo();
# set password not to expire
#wmic USERACCOUNT WHERE "Name = '$username'" SET Passwordexpires=FALSE
# Add to groups
$group = [ADSI]"WinNT://./Power Users,group"
$group.Add("WinNT://$username,user")
$group = [ADSI]"WinNT://./WW_Users,group"
$group.Add("WinNT://$username,user")
$ou = [adsi]"WinNT://127.0.0.1"
$user = $ou.psbase.get_children().find($username)
$user.PSBase.InvokeSet("TerminalServicesInitialProgram", "C:\Program Files (x86)\Wonderware\InTouch\view.exe c:\program files (x86)\archestra\framework\bin\sibanyegold-kdce_app_tse1_test")
$user.PSBase.InvokeSet("MaxConnectionTime", 120)
$user.PSBase.InvokeSet("MaxDisconnectionTime", 1)
$user.PSBase.InvokeSet("MaxIdleTime", 30)
$user.PSBase.InvokeSet("BrokenConnectionAction", 1)
$user.PSBase.InvokeSet("ReconnectionAction", 1)
$user.PSBase.InvokeSet("FullName", $fullname)
$user.setinfo()
}

Can you create an anonymous-access Windows share all from PowerShell?

Windows makes it difficult to create a network share with anonymous access (in other words, users who the share-hosting machine does not know about can access). The net share ShareName=C:\DesiredShareSource /GRANT:EVERYONE,FULL gives access to Everyone, but that does not include anonymous access (e.g. non-domain joined users, WITHOUT prompting credentials).
I know there's a way to do this from a GUI (https://serverfault.com/questions/272409/setting-up-an-anonymous-windows-server-2008-network-share), but is there a way changing security policies and creating anonymous network shares can be done strictly from PowerShell?
EDIT
This is what happens when I run the WMI script posted by Ansgar Wiechers. I get an exception but the share mounts successfully:
However, when I try and connect to the share from another box on the same network, I am still prompted for a username and password, as seen below:
Again, I want anonymous access (no username and password) to be set up all from command line.
Here is the exact code I am using in testingAnonShare.ps1, on a Win7 system:
$path = 'C:\Users\<REDACTED>\Desktop\Attempt'
$name = 'testinganon'
$description = 'share description'
function Get-Trustee($sid) {
$trustee = ([wmiclass]'Win32_Trustee').CreateInstance()
$trustee.SID = ([wmi]"Win32_SID.SID='$sid'").BinaryRepresentation
return $trustee
}
function New-FullAce($sid) {
$ace = ([wmiclass]'Win32_ACE').CreateInstance()
$ace.AccessMask = 2032127 # full control
$ace.AceFlags = 3 # container inherit + object inherit
$ace.AceType = 0 # access allowed
$ace.Trustee = Get-Trustee $sid
return $ace
}
$sd = ([wmiclass]'Win32_SecurityDescriptor').CreateInstance()
$sd.ControlFlags = 4
$sd.DACL = (New-FullAce 'S-1-1-0'),
(New-FullAce 'S-1-5-7')
$wmi = Get-WmiObject Win32_Share -List
$wmi.Create($path, $name, 0, $null, $description, '', $sd) | Out-Null
All examples create a share called test mapped to a path D:\test, granting full access to Anonymous and Everyone.
Windows Server 2012 R2 and newer
To create a share with everyone having Full access this is the command
New-SmbShare -Name 'test' -path 'D:\test' -FullAccess 'ANONYMOUS LOGON','Everyone'
To update an existing share to have the same permission is a little more complicated. First, assume the share name is test. Here is the code to change it to the same permissions as above.
Get-SmbShare -Name test |
Set-SmbShare -SecurityDescriptor 'O:BAG:DUD:(A;;FA;;;AN)(A;;FA;;;WD)'
To get the SecurityDescriptor string, create a share test like you want it and run the following command.
(get-smbshare -Name Test).SecurityDescriptor
Backward compatible (NET SHARE)
This can also be done with net share
net share test=D:\test /GRANT:"ANONYMOUS LOGON,FULL" /GRANT:"Everyone,FULL"
In addition to New-SmbShare (Windows Server 2012 or newer) and net share you can also use WMI for creating network shares.
$path = 'C:\DesiredShareSource'
$name = 'sharename'
$description = 'share description'
function Get-Trustee($sid) {
$trustee = ([wmiclass]'Win32_Trustee').CreateInstance()
$trustee.SID = ([wmi]"Win32_SID.SID='$sid'").BinaryRepresentation
return $trustee
}
function New-FullAce($sid) {
$ace = ([wmiclass]'Win32_ACE').CreateInstance()
$ace.AccessMask = 2032127 # full control
$ace.AceFlags = 3 # container inherit + object inherit
$ace.AceType = 0 # access allowed
$ace.Trustee = Get-Trustee $sid
return $ace
}
$sd = ([wmiclass]'Win32_SecurityDescriptor').CreateInstance()
$sd.ControlFlags = 4
$sd.DACL += (New-FullAce 'S-1-1-0').PSObject.BaseObject
$sd.DACL += (New-FullAce 'S-1-5-7').PSObject.BaseObject
$wmi = Get-WmiObject Win32_Share -List
$wmi.Create($path, $name, 0, $null, $description, '', $sd) | Out-Null
S-1-1-0 and S-1-5-7 are the well-known SIDs of the Everyone and Anonymous groups respectively.
Appending each ACE separately to the DACL property is required to make the code work with PowerShell v2. In more recent version you can assign the ACEs as an array, and you also don't need to unwrap the base object:
$sd.DACL = (New-FullAce 'S-1-1-0'), (New-FullAce 'S-1-5-7')
To actually enable anonymous access to shares you also need to make three changes to the local security policy (source):
Start secpol.msc.
Navigate to Security Settings → Local Policies → Security Options.
Change the following settings:
Accounts: Guest account status → Enabled
Network access: Let Everyone permissions apply to anonymous users → Enabled
Network access: Shares that can be accessed anonymously → sharename
Note that I did not have to change the setting Network access: Restrict anonymous access to Named Pipes and Shares to enable anonymous access from Windows 7 to an anonymous share on Server 2012 R2, but I did have to add NTFS permissions for the Everyone group.
$acl = Get-Acl -Path 'C:\DesiredShareSource'
$ace = New-Object Security.AccessControl.FileSystemAccessRule(
'Everyone', 'ReadAndExecute', 'ContainerInherit, ObjectInherit', 'None', 'Allow'
)
$acl.AddAccessRule($ace)
Get-Acl -Path 'C:\DesiredShareSource' -AclObject $acl
I'm not aware of a way to make the policy changes with a script. You may be able to cook up something by wrapping secedit in PowerShell, but whenever I had to deal with secedit it turned out to be … bothersome, so I wouldn't recommend it. In a domain environment you can deploy local security policy settings via group policies, though.

How to compare age of local file with file on FTP server and download if remote copy is newer in PowerShell

I'm in the process of writing a PowerShell script to help in the process of setting up new PC's for my work. This will hopefully be used by more than just me so I'm trying to think of everything.
I have offline installers (java, flash, reader, etc) saved on our FTP server that the script downloads if a local copy hasn't already been saved in the Apps directory that gets created. Periodically the files on the FTP server will get updated as new versions of the programs are released. I want the script to have an option of checking for newer versions of the installers in case someone likes to carry around the local copies and forgets to check the server every now and then. It also will need to work in Windows 7 without any need to import additional modules unless there's an easy way to do that on multiple PC's at a time. I know about the import command, but the experiences I've had needed me to copy the module files into multiple places on the PC before it'd work.
Right now I haven't had much luck finding any solutions. I've found code that checks for modified dates on local files, or files on a local server, but nothing that deals with FTP other than uploading\downloading files.
Here's the last thing I tried. I tried a combination of what I found for local files with FTP. Didn't work too well.
I'm new to PowerShell, but I've been pretty good at piecing this whole thing together so far. However, this idea is becoming troublesome.
Thank you for the help.
$ftpsite = "ftp://ftpsite.com/folder/"
$firefox = (Get-Item $dir\Apps\install_firefox.exe).LastWriteTime.toString("MM/dd/yyyy")
if ($firefoxftp = (Get-ChildItem $ftpsite/install_firefox.exe | Where{$_.LastWriteTime -gt $firefox})) {
$File = "$dir\Apps\install_firefox.exe"
$ftp = "ftp://ftpsite.com/folder/install_firefox.exe"
$webclient = New-Object System.Net.WebClient
$uri = New-Object System.Uri($ftp)
$webclient.DownloadFile($uri, $File)
}
UPDATE:
Here's what I have after Martin's help. It kind of works. It downloads the file from FTP, but it's not comparing the remote and local correctly. The remote file returns 20150709140505 and the local file returns 07/09/2015 2:05:05 PM. How do I format one to look like the other before the comparison, and is "-gt" the correct comparison to use?
Thanks!
function update {
$ftprequest = [System.Net.FtpWebRequest]::Create("ftp://ftpsite.com/Script_Apps/install_firefox.exe")
$ftprequest.Method = [System.Net.WebRequestMethods+Ftp]::GetDateTimestamp
$response = $ftprequest.GetResponse().StatusDescription
$tokens = $response.Split(" ")
$code = $tokens[0]
$localfile = (Get-Item "$dir\Apps\install_firefox.exe").LastWriteTimeUtc
if ($tokens -gt $localfile) {
write-host "Updating Firefox Installer..."
$File = "$dir\Apps\install_firefox.exe"
$ftp = "ftp://ftpsite.com/Script_Apps/install_firefox.exe"
$webclient = New-Object System.Net.WebClient
$uri = New-Object System.Uri($ftp)
$webclient.DownloadFile($uri, $File)
"Updated Firefox" >> $global:logfile
mainmenu
}
else {
Write-Host "Local Copy is Newer."
sleep 3
mainmenu
}
}
UPDATE 2:
Seems to be working! Here's the code. Thanks for the help!
function update {
$ftprequest = [System.Net.FtpWebRequest]::Create("ftp://ftpserver.com/Script_Apps/install_firefox.exe")
$ftprequest.Method = [System.Net.WebRequestMethods+Ftp]::GetDateTimestamp
$response = $ftprequest.GetResponse().StatusDescription
$tokens = $response.Split(" ")
$code = $tokens[0]
$localtime = (Get-Item "$dir\Apps\install_firefox.exe").LastWriteTimeUtc
if ($code -eq 213) {
$tokens = $tokens[1]
$localtime = "{0:yyyymmddHHmmss}" -f [datetime]$localtime
}
if ($tokens -gt $localtime) {
write-host "Updating Firefox Installer..."
$File = "$dir\Apps\install_firefox.exe"
$ftp = "ftp://ftpserver.com/Script_Apps/install_firefox.exe"
$webclient = New-Object System.Net.WebClient
$uri = New-Object System.Uri($ftp)
$webclient.DownloadFile($uri, $File)
"Updated Firefox" >> $global:logfile
mainmenu
}
else {
Write-Host "Local Copy is Newer."
sleep 3
mainmenu
}
}
You cannot use the WebClient class to check remote file timestamp.
You can use the FtpWebRequest class with its GetDateTimestamp FTP "method" and parse the UTC timestamp string it returns. The format is specified by RFC 3659 to be YYYYMMDDHHMMSS[.sss].
That would work only if the FTP server supports MDTM command that the method uses under the cover (most servers do, but not all).
$url = "ftp://ftpsite.com/folder/install_firefox.exe"
$ftprequest = [System.Net.FtpWebRequest]::Create($url)
$ftprequest.Method = [System.Net.WebRequestMethods+Ftp]::GetDateTimestamp
$response = $ftprequest.GetResponse().StatusDescription
$tokens = $response.Split(" ")
$code = $tokens[0]
if ($code -eq 213)
{
Write-Host "Timestamp is" $tokens[1]
}
else
{
Write-Host "Error" $response
}
It would output something like:
Timestamp is 20150709065036
Now you parse it, and compare against a UTC timestamp of a local file:
(Get-Item "install_firefox.exe").LastWriteTimeUtc
Or save yourself some time and use an FTP library/tool that can do this for you.
For example with WinSCP .NET assembly, you can synchronize whole remote folder with installers with a local copy with one call to the Session.SynchronizeDirectories. Or your can limit the synchronization to a single file only.
# Load WinSCP .NET assembly
Add-Type -Path "WinSCPnet.dll"
# Setup session options
$sessionOptions = New-Object WinSCP.SessionOptions
$sessionOptions.Protocol = [WinSCP.Protocol]::Ftp
$sessionOptions.HostName = "ftpsite.com"
$session = New-Object WinSCP.Session
# Connect
$session.Open($sessionOptions)
$transferOptions = New-Object WinSCP.TransferOptions
# Synchronize only this one file.
# If you remove the file mask, all files in the folder are synchronized:
$transferOptions.FileMask = "install_firefox.exe"
$session.SynchronizeDirectories(
[WinSCP.SynchronizationMode]::Local, "$dir\Apps", "/folder",
$False, $False, [WinSCP.SynchronizationCriteria]::Time,
$transferOptions).Check()
To use the assembly, just extract a contents of .NET assembly package to your script folder. No other installation is needed.
The assembly supports not only the MDTM, but also other alternative methods to retrieve the timestamp.
See also a related Powershell example that shows both the above code and other techniques.
(I'm the author of WinSCP)

PowerShell - If/Else Statement Doesn't Work Properly

First off I apologize for the extremely long, wordy post. It’s an interesting issue and I wanted to be as detailed as possible. I’ve tried looking through any related PowerShell posts on the site but I couldn’t find anything that helped me with troubleshooting this problem.
I've been working on a PowerShell script with a team that can send Wake-On-Lan packets to a group of computers. It works by reading a .csv file that has the hostnames and MAC’s in two columns, then it creates the WOL packets for each computer and broadcasts them out on the network. After the WOL packets are sent, it waits a minute and then pings the computers to verify they are online, and if any don’t respond it will display a window with what machines didn’t respond to a ping. Up until the final If/Else statement works fine, so I won't be going into too much detail on that part of the script (but of course if you want/need further details please feel free to ask).
The problem I’m having is with the final If/Else statement. The way the script is supposed to work is that in the ForEach loop in the middle of the script, the value of variable $PingResult is true or false depending on whether or not the computer responds to a ping. If the ping fails, $PingResult is $false, and then it adds the hostname to the $PingResult2 variable.
In theory if all of the machines respond, the If statement fires and the message box displays that it was a success and then the script stops. If any machines failed to respond, the Else statement runs and it joins all of the items together from the $PingResult2 variable and displays the list in a window.
What actually happens is that even if all of the machines respond to a ping, the If statement is completely skipped and the Else statement runs instead. However, at that point the $PingResult2 variable is blank and hence it doesn’t display any computer names of machines that failed to respond. In my testing I’ve never seen a case where the script fails to wake a computer up (assuming it’s plugged in, etc.), but the Else statement still runs regardless. In situations where the Else statement runs, I’ve checked the value of the $PingResult2 variable and confirmed that it is blank, and typing $PingResult2 –eq “” returns $true.
To add another wrinkle to the problem, I want to return to the $PingResult2 variable. I had to create the variable as a generic list so that it would support the Add method to allow the variable to grow as needed. As a test, we modified the script to concatenate the results together by using the += operator instead of making $PingResult2 a list, and while that didn’t give a very readable visual result in the final display window if machines failed, it did actually work properly occasionally. If all of the computers responded successfully the If statement would run as expected and display the success message. Like I said, it would sometimes work and sometimes not, with no other changes making a difference in the results. One other thing that we tried was taking out all of the references to the Visual Basic assembly and other GUI elements (besides the Out-GridView window) and that didn’t work either.
Any idea of what could be causing this problem? Me and my team are completely tapped out of ideas at this point and we’d love to figure out what’s causing the issue. We’ve tried it on Windows 7, 8.1, and the latest preview release of Windows 10 with no success. Thanks in advance for any assistance.
P.S Extra brownie points if you can explain what the regular expression on line 29 is called and how it exactly works. I found out about it on a web posting that resolved the issue of adding a colon between every two characters, but the posting didn’t explain what it was called. (Original link http://powershell.org/wp/forums/topic/add-colon-between-every-2-characters/)
Original WOL Script we built the rest of the script around was by John Savill (link http://windowsitpro.com/networking/q-how-can-i-easily-send-magic-packet-wake-machine-my-subnet)
Script
Add-Type -AssemblyName Microsoft.VisualBasic,System.Windows.Forms
$OpenFileDialog = New-Object System.Windows.Forms.OpenFileDialog
$OpenFileDialog.ShowDialog() | Out-Null
$FileVerify = Get-Content -Path $OpenFileDialog.FileName -TotalCount 1
$FileVerify = ($FileVerify -split ',')
If($FileVerify[0] -ne "Machine Name" -or $FileVerify[1] -ne "MAC")
{
$MsgBox = [System.Windows.Forms.MessageBox]::Show("The CSV File's headers must be Machine Name and MAC.",'Invalid CSV File headers!',0,48)
Break
}
$ComputerList = Import-Csv -Path $OpenFileDialog.FileName |
Out-GridView -PassThru -Title "Select Computers to Wake up"
ForEach($Computer in $ComputerList)
{
If($Computer.'MAC' -notmatch '([:]|[-])')
{
$Computer.'MAC' = $Computer.'MAC' -replace '(..(?!$))','$1:'
}
$MACAddr = $Computer.'MAC'.split('([:]|[-])') | %{ [byte]('0x' + $_) }
$UDPclient = new-Object System.Net.Sockets.UdpClient
$UDPclient.Connect(([System.Net.IPAddress]::Broadcast),4000)
$packet = [byte[]](,0xFF * 6)
$packet += $MACAddr * 16
[void] $UDPclient.Send($packet, $packet.Length)
write "Wake-On-Lan magic packet sent to $($Computer.'Machine Name'.ToUpper())"
}
Write-Host "Pausing for sixty seconds before verifying connectivity."
Start-Sleep -Seconds 60
$PingResult2 = New-Object System.Collections.Generic.List[System.String]
ForEach($Computer in $ComputerList)
{
Write-Host "Pinging $($Computer.'Machine Name')"
$PingResult = Test-Connection -ComputerName $Computer.'Machine Name' -Quiet
If ($PingResult -eq $false)
{
$PingResult2.Add($Computer.'Machine Name')
}
}
If($PingResult2 -eq "")
{
[System.Windows.Forms.MessageBox]::Show("All machines selected are online.",'Success',0,48)
Break
}
Else
{
$PingResult2 = ($PingResult2 -join ', ')
[System.Windows.Forms.MessageBox]::Show("The following machines did not respond to a ping: $PingResult2",'Unreachable Machines',0,48)
}
The comparison in your If statement is incorrect because you are comparing $PingResult2, a List<string>, to a string. Instead, try
If ($PingResult2.Count -eq 0)
{
# Show the message box
}
Else
{
# Show the other message box
}
or one of countless other variations on this theme.
The regular expression in question uses a backreference to replace exactly two characters with the same two characters plus a colon character. I am unsure what exactly you are attempting to "define," though.
You are checking if a list has a value of a null string, rather than checking the number of items in the list.
If you change the if statement to the following it should work fine:
If($PingResult2.count -eq 0)
I'm guessing the regex is trying to insert a colon between every two characters of a string to represent 0123456789ab as 01:23:45:67:89:ab.
The code means if there is no hyphen or colon in the MAC, put in a colon every the characters, then split the address using colon as delimiter then represent each as a byte:
If($Computer.'MAC' -notmatch '([:]|[-])')
{
$Computer.'MAC' = $Computer.'MAC' -replace '(..(?!$))','$1:'
}
$MACAddr = $Computer.'MAC'.split('([:]|[-])') | %{ [byte]('0x' + $_) }
The other answer have explained quite well why your code does not work. I'm not going there. Instead I'll give some suggestions that I think would improve your script, and explain why I think so. Let's start with functions. Some of the things you do are functions I keep on hand because, well, they work well and are used often enough that I like having them handy.
First, your dialog to get the CSV file path. It works, don't get me wrong, but it could probably be better... As it is you pop up an Open File dialog with no parameters. This function allows you to use a few different parameters as wanted, or none for a very generic Open File dialog, but I think it's a slight improvement here:
Function Get-FilePath{
[CmdletBinding()]
Param(
[String]$Filter = "|*.*",
[String]$InitialDirectory = "C:\")
[void][System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms")
$OpenFileDialog = New-Object System.Windows.Forms.OpenFileDialog
$OpenFileDialog.initialDirectory = $InitialDirectory
$OpenFileDialog.filter = $Filter
[void]$OpenFileDialog.ShowDialog()
$OpenFileDialog.filename
}
Then just call it as such:
$CSVFile = Get-FilePath -Filter "Comma Separated Value (.CSV)|*.CSV" -InitialDirectory "$env:USERPROFILE\Desktop"
That opens the dialog filtering for only CSV files, and starts them looking at their desktop (I find that a lot of people save things to their desktop). That only gets the path, so you would run your validation like you were. Actually, not like you were. You really seem to have over complicated that whole bit. Bit I'll get to that in a moment, first, another function! You call message boxes fairly often, and type out a bunch of options, and call the type, and everything every single time. If you're going to do it more than once, make it easy on yourself, make a function. Here, check this out:
Function Show-MsgBox ($Text,$Title="",[Windows.Forms.MessageBoxButtons]$Button = "OK",[Windows.Forms.MessageBoxIcon]$Icon="Information"){
[Windows.Forms.MessageBox]::Show("$Text", "$Title", [Windows.Forms.MessageBoxButtons]::$Button, $Icon) | ?{(!($_ -eq "OK"))}
}
Then you can specify as much or as little as you want for it. Plus it uses Type'd parameters, so tab completion works, or in the ISE (if that's where you're writing your script, like I do) it will pop up valid options and you just pick from a list for the buttons or icon to show. Plus it doesn't return anything if it's a simple 'OK' response, to keep things clean, but will return Yes/No/Cancel or whatever other option you choose for buttons.
Ok, that's the functions, let's get to the meat of the script. Your file validation... Ok, you pull the first line of the file, so that should just be a string, I'm not sure why you're splitting it and verifying each header individually. Just match the string as a whole. I would suggest doing it case insensitive, since we don't really care about case here. Also, depending on how the CSV file was generated, there could be quotes around headers, which you may want to account for. Using -Match will perform a RegEx match that is a bit more forgiving.
If((Get-Content $CSVFile -TotalCount 1) -match '^"?machine name"?,"?mac"?$'){
Show-MsgBox "The CSV File's headers must be Machine Name and MAC." 'Invalid CSV File headers!' -Icon Warning
break
}
So now we have two functions, and 5 lines of code. Yes, the functions take up more space than what you previously had, but they're friendlier to work with, and IMO more functional. Your MAC address correction, and WOL sending part are all aces so far as I'm concerned. There's no reason to change that part. Now, for validating that computers came back up... here we could use some improvement. Instead of making a [List] just add a member to each object, then filter against that below. The script as a whole would be a little longer, but better off for it I think.
Add-Type -AssemblyName Microsoft.VisualBasic,System.Windows.Forms
Function Get-FilePath{
[CmdletBinding()]
Param(
[String]$Filter = "|*.*",
[String]$InitialDirectory = "C:\")
[void][System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms")
$OpenFileDialog = New-Object System.Windows.Forms.OpenFileDialog
$OpenFileDialog.initialDirectory = $InitialDirectory
$OpenFileDialog.filter = $Filter
[void]$OpenFileDialog.ShowDialog()
$OpenFileDialog.filename
}
Function Show-MsgBox ($Text,$Title="",[Windows.Forms.MessageBoxButtons]$Button = "OK",[Windows.Forms.MessageBoxIcon]$Icon="Information"){
[Windows.Forms.MessageBox]::Show("$Text", "$Title", [Windows.Forms.MessageBoxButtons]::$Button, $Icon) | ?{(!($_ -eq "OK"))}
}
#Get File Path
$CSVFile = Get-FilePath -Filter "Comma Separated Value (.CSV)|*.CSV" -InitialDirectory "$env:USERPROFILE\Desktop"
#Validate Header
If((Get-Content $CSVFile -TotalCount 1) -match '^"?machine name"?,"?mac"?$'){
Show-MsgBox "The CSV File's headers must be Machine Name and MAC." 'Invalid CSV File headers!' -Icon Warning
break
}
$ComputerList = Import-Csv -Path $CSVFile |
Out-GridView -PassThru -Title "Select Computers to Wake up"
ForEach($Computer in $ComputerList)
{
If($Computer.'MAC' -notmatch '([:]|[-])')
{
$Computer.'MAC' = $Computer.'MAC' -replace '(..(?!$))','$1:'
}
$MACAddr = $Computer.'MAC'.split('([:]|[-])') | %{ [byte]('0x' + $_) }
$UDPclient = new-Object System.Net.Sockets.UdpClient
$UDPclient.Connect(([System.Net.IPAddress]::Broadcast),4000)
$packet = [byte[]](,0xFF * 6)
$packet += $MACAddr * 16
[void] $UDPclient.Send($packet, $packet.Length)
write "Wake-On-Lan magic packet sent to $($Computer.'Machine Name'.ToUpper())"
}
Write-Host "Pausing for sixty seconds before verifying connectivity."
Start-Sleep -Seconds 60
$ComputerList|ForEach
{
Write-Host "Pinging $($_.'Machine Name')"
Add-Member -InputObject $_ -NotePropertyName "PingResult" -NotePropertyValue (Test-Connection -ComputerName $Computer.'Machine Name' -Quiet)
}
If(($ComputerList|Where{!($_.PingResult)}).Count -gt 0)
{
Show-MsgBox "All machines selected are online." 'Success'
}
Else
{
Show-MsgBox "The following machines did not respond to a ping: $(($ComputerList|?{!($_.PingResult)}) -join ", ")" 'Unreachable Machines' -Icon Asterisk
}
Ok, I'm going to get off my soap box and go home, my shift's over and it's time for a cold one.

Resources