- WHAT
- A homelab laptop wakes my gaming PC with Wake-on-LAN, runs heavy jobs on it over SSH, and shuts it down again
- STACK
- Wake-on-LAN · OpenSSH on Windows · bash · Tailscale
- LINKS
- repo
the itch
I have two machines that complement each other in a slightly annoying way.
The laptop has the uptime. It runs the homelab and is on all day. The gaming PC has the real GPU, a lot of RAM and the fans of a small drone, and it costs power and noise every hour it’s awake. I only need it for a few of those hours.
So the laptop is the always-on boss and the PC is the heavy worker, asleep until there’s a job. Wake it, hand it the job, read the output from my chair, switch it off again. Is that overkill for one person? Yes. That’s why it’s in this series.
how it works
pcctlis a small bash script with three verbs:on,offandstatus.onsends the magic packet.statusjust checks whether port 22 answers.pcpywakes the PC if it has to, waits, copies the project over and runs a script there, streaming the output back.
The magic packet
Wake-on-LAN is older and dumber than it sounds. A magic packet is 6 bytes of 0xFF followed by the target’s MAC address repeated 16 times, sent as a UDP broadcast. The network card stays powered while the PC is off and watches for that pattern. No handshake, no authentication, no reply.
A broadcast stays inside one network segment, so the sender has to live on the same LAN. Here that’s easy: the laptop is on Wi-Fi, the PC is wired, and both share a subnet.
The card also needs permission to listen: a firmware setting (often Wake on LAN or Power On By PCI-E) and the adapter’s power options in Windows. Names vary by board and driver. Fast Startup is the usual suspect when wake works from sleep but not from a full shutdown. On this PC its flag is on but hibernation is off, and Fast Startup depends on hibernation, so it does nothing.
Port 22 is not “ready”
Pinging the PC is the wrong way to tell if it’s awake, because the network can come up before sshd does. pcctl status checks port 22 instead. pcpy polls it for up to three minutes, then waits five more seconds before it tries anything. Nothing has gone wrong without that pause. It’s a margin, because an open port doesn’t mean the machine is ready for work.
Running the job on Windows
pcpy copies the project’s src folder over with scp, then runs one script through SSH. Three details of that last line are worth stealing:
- It calls the venv’s Python by path.
.venv\Scripts\python.exeinstead of activating the environment, which is one less thing to go wrong in a non-interactive shell. set VAR=value&& nexthas no space before the&&. Incmd, a space there becomes part of the value.- The output goes through
tr -d '\0'. Windows tools sometimes emit UTF-16, which shows up as NUL bytes on a Linux terminal.
Two keys, one dangerous
There are two SSH keys for the PC. The work key is for pcpy. The other is only for pcctl off, so the everyday key can’t do the dangerous thing by accident. A comment at the top of pcpy says it plainly: “Never use id_pc: that key shuts the PC down.”
pcctl off runs ssh with no command at all, so the server side must be doing the work. The usual way is to lock that key to one forced command in the PC’s authorized_keys. The repo’s README has an example. And because the laptop is on Tailscale, my iPhone has Shortcuts for the same three verbs: on, off and status.
known gaps
Nothing broke while I built this, which makes me suspicious. Here is where I’d look first.
- The job runs in the foreground of an SSH session. If the connection drops, it probably goes with it. Running it detached, or as a scheduled task, is the fix.
pcpydoesn’t switch the PC off.pcctl offdoes, as a separate step I have to remember. The plan is apcrunthat wakes, syncs, runs, pulls results back and shuts down.- Wake-on-LAN has no security. Anyone on the LAN can send the packet. That’s fine at home, and it stays off the internet.
- An AMD GPU on Windows can be a bumpy road for ML frameworks. Check what your stack supports first.
- I haven’t timed the wake. I don’t have a number from
pcctl onto SSH answering, and I won’t make one up.
steal this
pcctl is the whole control surface, with my network details replaced by placeholders. The working scripts and a README are in the pc-over-lan repo.
#!/usr/bin/env bash
PC_IP=[PC IP]
PC_MAC=[PC MAC]
BCAST=[LAN BROADCAST ADDRESS]
case "$1" in
on) wakeonlan -i "$BCAST" "$PC_MAC" ;;
off) ssh -i ~/.ssh/id_pc -o BatchMode=yes -o ConnectTimeout=5 [USER]@"$PC_IP" ;;
status) nc -z -w 2 "$PC_IP" 22 && echo up || echo down ;;
*) echo "usage: pcctl on|off|status" ;;
esacpcpy wakes the PC if it’s down, waits for it, syncs the code and runs the script on the other side.
#!/usr/bin/env bash
# pcpy <script.py> [args...] : sync the project to the PC and run a script there.
# Uses the work key. Never use id_pc: that key shuts the PC down.
K=~/.ssh/id_pc_work; H=[USER]@[PC IP]; IP=[PC IP]
O="-i $K -o IdentitiesOnly=yes -o BatchMode=yes -o ConnectTimeout=8"
if ! nc -z -w2 $IP 22 2>/dev/null; then
echo "[pcpy] waking PC..." >&2; pcctl on >/dev/null 2>&1
for i in $(seq 1 60); do nc -z -w2 $IP 22 2>/dev/null && break; sleep 3; done
sleep 5
fi
[ -n "$1" ] || { echo "usage: pcpy <script.py> [args...]" >&2; exit 2; }
script=$1; shift
scp -q $O -r ~/[PROJECT]/src $H:C:/[PROJECT]/ || { echo "[pcpy] scp failed" >&2; exit 1; }
ssh $O $H "cmd /c \"set DATA_DIR=C:\\[PROJECT]-data&& cd /d C:\\[PROJECT]&& .venv\\Scripts\\python.exe src\\$script $* 2>&1\"" | tr -d '\0'what i’d do differently
- Detach the job from the SSH session. A dropped Wi-Fi link shouldn’t kill an hours-long run.
- Finish
pcrun. Wake, sync, run, pull results, shut down, in one command. Then there’s nothing to forget. - Light the desk strip while the PC works. The strip from the other post already knows how to show a colour. A busy PC deserves one too.
- Time the wake, with one log line per run, to see whether the PC is slowly getting slower to boot.