PowerShell 7 Wake on LAN

Being somewhat lazy I find it convenient to manage my small home LAN from my desktop. Most of the machines are Windows 10 systems, along with a Debian-based Raspberry Pi, so Remote Desktop provides all I need in the way of remote GUI access.

But to do backup and system updates the machines have to be awake. Which is where wake-on-LAN comes into play.

For years I used a simple tool on the Pi to send the necessary magic packet to a target machine to wake it up. But that required my to remotely access the Pi, via Putty, log into it and run the wake script. Why not do that from Windows Terminal using a PowerShell script? I rarely use PowerShell but it’s conceptually based on the .NET framework and C# so how hard could it be to set up a simple wake-on-LAN script?

Well, harder than I expected. Because “based on” is both a blessing and a curse if you’re a long-time avid C# developer like me. It means it’s similar enough to what you already know that you think you understand what you’re doing…but it’s different enough that you actually don’t. The devil is always in the details and the details of PowerShell are pretty different from C#.

Beyond learning a related-but-different language I also ran into a complication regarding just writing the code. I do virtually all my coding in Visual Studio (2019) and love it. It claims to be able to author PowerShell scripts — and there’s even an add-on from IronMan Software which adds a PowerShell project type and some other purportedly nifty capabilities — but behind the scenes…it’s a mess. Because so far as I can tell Visual Studio 2019, even with all the latest patches and updates installed, insists on using PowerShell 5, even if you have PowerShell 7 installed on your system. Which I do.

PowerShell 7 and PowerShell 5 are related but very different. The former is based on .NET Core (which is now the official future of .NET) while the latter is based on the original .NET Framework. And the differences are large enough they got in the way of having the code I pasted from various sites claiming they’d solved parts of the wake problem from working.

There may be a way to convince Visual Studio 2019 to use a modern version of PowerShell but I could not find it despite several hours of searching and trying. Chalk up another entry in the immensely long “don’t the various parts of Microsoft ever talk to each other?” list.

My solution was to use a different/free Microsoft editor, Visual Studio Code. Which Microsoft bills as, among other things, the “official” way to write PowerShell scripts. It, at least, is smart enough to use the latest version of PowerShell if you have it installed.

Here’s the script, in all its glory:

using namespace System.Text.Json

class Device {
	static [string]$BroadcastIP = '192.168.1.255'
	[string]$Name
	[string]$MacAddress

	[Byte[]]GetByteArray(){
		$macByteArray = $this.MacAddress -split "[:-]" | ForEach-Object { [Byte] "0x$_"}
		return (,0xFF * 6) + ($macByteArray * 16)
	}

	[void]Wake(){
		$macByteArray = $this.MacAddress -split "[:-]" | ForEach-Object { [Byte] "0x$_"}
		$magicPacket = (,0xFF * 6) + ($macByteArray * 16)

		$udpClient = New-Object System.Net.Sockets.UdpClient
		
		#$udpClient.Connect(([System.Net.IPAddress]::Broadcast), 9)
		# I have no idea why the default global address target doesn't work... but it sure doesn't!
		# no doubt some "feature" of Windows. Thanx, Microsoft :(
		$udpClient.Connect([Device]::BroadcastIP, 9)
		
		$udpClient.Send($magicPacket,$magicPacket.Length)

		$udpClient.Close()
		$udpClient.Dispose()
	}
}

class Configuration {
	[Device[]] $Devices

	[void]Wake([string]$name){
		$device = $this.Devices | Where-Object Name -eq $name

		if( $null -eq $device ){
			Write-Error "No device named $name on file"
		}
		else{
			$device.Wake()
			Write-Information -InformationAction Continue "Sent magic packet to $name at $($device.MacAddress) via $([Device]::BroadcastIP)"
		}
	}
}

function Open-Device {
	Param(
		[Parameter(Mandatory=$true, Position=0)]
		[string]$DeviceName
	)

	$config = [JsonSerializer]::Deserialize((Get-Content "c:/Users/Mark/PowerShell/Wake-Device/devices.json"), [Configuration], $null)

	$config.Wake($DeviceName)
}

Export-ModuleMember -Function Open-Device

Then the real fun, getting this simple script to actually work, began. I won’t go through all the stumbles because they were mostly due to that “based on C#” conceptual glitch I mentioned earlier.

But I will point out my favorite problem (slowly counts to 20, not 10, to keep from exploding as he recalls how immensely frustrating it was). That simple call on line 22 stubbornly refused to work in its initial form, which you can see commented out on line 19. Apparently, Microsoft doesn’t like you broadcasting to the official UDP broadcast address (255.255.255.255). Switching to the LAN-local broadcast address (192.168.1.255, in my case) was an easy fix…but it took a l-o-n-g time to figure out why code other people swore worked didn’t. In fact, I had to attach WireShark to my development machine and attempt to broadcast wake up packets to it before I realized the packets just weren’t getting out.

No doubt there’s some safety or security reason for this “feature”. But it sure was a PITA…and not well-documented. At least I never came across reference to the problem in quite a bit of searching.

If you create a PowerShell module out of this code — which is what I did — make sure you install it in a place where PowerShell can find it. On my system one of those locations is c:\Program Files\PowerShell\Modules:

Enjoy!

Leave a Comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Archives
Categories