Automated backup script unable to find the required window despite successful Get-Process

I am trying to create a script to run in powershell that accomplishes the following steps for a game server that I am hosting:
- save the world data (this must be done in the server console)
- shutdown the server console (this must be done in the server console)
- copy all world data
- copy most recent log files
- zip both of these into a backup file
- delete any old backups (older than 7 days)
- restart the game server
- notify me of successful completion
- update a log file with completion status
most of the process works, except for steps 1 and 2 as powershell can't seem to find the window that it needs to pull to the front in order to send the saveworld and shutdown commands.
here is the code that I am struggling with:
# Load .NET Windows Forms for SendKeys support
Add-Type -AssemblyName System.Windows.Forms
# === BRING SERVER CONSOLE TO FOREGROUND ===
# This uses user32.dll to find and focus the console window before sending commands
Add-Type @'
using System;
using System.Runtime.InteropServices;
public class Win32 {
[DllImport("user32.dll")]
public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
[DllImport("user32.dll")]
public static extern bool SetForegroundWindow(IntPtr hWnd);
}
'@
# Replace this with the actual window title of your dedicated server console
$windowTitle = (Get-Process | Where-Object { $_.MainWindowTitle -Like '*7 Days to Die*' }).MainWindowTitle
# Get the window handle
$hWnd = [Win32]::FindWindow($null, $windowTitle)
# Bring the window to the foreground
if ($hWnd -ne [IntPtr]::Zero) {
[Win32]::SetForegroundWindow($hWnd)
Start-Sleep -Seconds 1 # Small delay to ensure focus is set
}
else {
Write-Host "⚠️ Could not find server window titled '$windowTitle'. Commands may not work."
}
The first issue that I ran into was with trying to find the name of the window as originally the $windowTitle variable was just a string that I had input as a guess. I used Get-Process and it returned "LADs - Port 26900 - Running - 7 Days to Die Dedicated Server Console"" as the window title, so I put that in the variable instead. That didn't work either so I went down a whole rabbit hole of trying to EnumerateName which ended up being a dead end, then tried a tasklist -v command which ended up confirming once again that the window title is exactly what Get-Process had already grabbed for me. Finally I tried inject the Get-Process directly into the script as the $windowTitle variable instead of trying to manually create the variable and that was where it got weird. Powershell returned : "Could not find server window titled 'LADs - Port 26900 - Running - 7 Days to Die Dedicated Server Console'. Commands may not work." This tells me that Powershell found the window title (because I didn't input that title, it grabbed it with Get-Process), but it still wasn't able to execute successfully. I've also downloaded Visual Studio and Spy++ and used Spy++ to confirm the window title. It shows the same.
My end goal with this section is to find the window and pull it to the front so that Powershell can use SendKeys to send the saveworld and shutdown commands to the console. unfortunately the script seems to get stumped at the FindWindow portion and continues to return: "⚠️ Could not find server window titled '$windowTitle'. Commands may not work." though it is successfully replacing the $windowTitle variable each time.
Answer
The problem is very likely the PowerShell coercion of $null
to String.Empty
instead of NullString.Value
, due to this, FindWindow
always fails because the first argument isn't actually NULL
.
To fix the problem, the correct call would've been:
$hWnd = [Win32]::FindWindow([NullString]::Value, $windowTitle)
Another way to solve it would be to change the signature and use IntPtr
as the first argument:
public static extern IntPtr FindWindow(IntPtr lpClassName, string lpWindowName);
Then in PowerShell you can use IntPtr.Zero
:
$hWnd = [Win32]::FindWindow([IntPtr]::Zero, $windowTitle)
Another thing to consider, you should ensure FindWindowW
is being called instead of FindWindowA
. For this, you should include CharSet.Unicode
in your DllImport
attribute.
In summary, these changes should fix the problem:
Add-Type '
using System;
using System.Runtime.InteropServices;
public static class Win32
{
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
[DllImport("user32.dll")]
public static extern bool SetForegroundWindow(IntPtr hWnd);
}'
# We don't know if more than one window could contain `7 Days to Die`
$processes = Get-Process |
Where-Object MainWindowTitle -Like '*7 Days to Die*'
# So we should loop thru the result or use `Select-Object -First 1`...
foreach ($process in $processes) {
$hWnd = [Win32]::FindWindow([NullString]::Value, $process.MainWindowTitle)
# if it succeeds to find a window
if ($hWnd -ne [IntPtr]::Zero) {
[Win32]::SetForegroundWindow($hWnd)
Start-Sleep -Seconds 1
# go to the next process / finishes the loop
continue
}
# if it failed, we can determine why with the following
$ex = [System.ComponentModel.Win32Exception]::new(
[System.Runtime.InteropServices.Marshal]::GetLastWin32Error())
Write-Error -Exception $ex
}