feat(windows-installer): native .exe installer pipeline

Brings the Windows installer work from dev to main without merging
  the rest of the 0.2.5b1 release content. Squashes 12 commits from
  dev (8711c54e..7bb11df2) into a single net-effect commit on main.

  Includes:
  - installers/windows/ — Inno Setup .iss script, build.py, vendored
    NSSM 2.24, bambuddy.ico (multi-resolution app icon), service
    install/uninstall .bat files, build pipeline README
  - backend/app/services/network_utils.py — Windows psutil branch so
    the VP bind-IP dropdown enumerates interfaces; Linux/macOS path
    unchanged
  - .github/workflows/windows-installer.yml — reconciles main's
    kludge-pushed copy with dev's accumulated changes (NSSM
    vendoring, version-from-tag, unversioned alias step, etc.)

  CHANGELOG and README entries for the Windows installer stay on
  dev — they reference unreleased 0.2.5b1 release notes that aren't
  on main yet.
This commit is contained in:
maziggy
2026-06-10 14:11:02 +02:00
parent 9d0d51af4e
commit f4dfe03a87
10 changed files with 860 additions and 1 deletions
+79 -1
View File
@@ -7,10 +7,14 @@ import shutil
import socket
import struct
import subprocess
import sys
logger = logging.getLogger(__name__)
# Interfaces to exclude from selection
# Interfaces to exclude from selection (Linux only — Windows adapter names
# don't follow these prefixes and there's no equivalent uniform Windows
# exclude list worth hard-coding; the psutil path filters on address class
# (loopback, link-local) and interface up-state instead).
EXCLUDED_INTERFACE_PREFIXES = ("lo", "docker", "br-", "veth", "virbr")
# Resolve full path to `ip` command (may not be in PATH for service users)
@@ -22,12 +26,86 @@ def _is_excluded(name: str) -> bool:
return any(name.startswith(prefix) for prefix in EXCLUDED_INTERFACE_PREFIXES)
def _get_network_interfaces_psutil() -> list[dict]:
"""Windows path: enumerate interfaces via psutil.
fcntl + ioctl is Linux-only, and the ``ip`` command isn't available
on Windows either, so both Linux code paths return empty here. psutil
is already a Bambuddy dep (``psutil>=6.0.0``) and gives us cross-
platform name + IPv4 + netmask in one call.
Filters: IPv4 only (matches the Linux path), skip loopback and
link-local (169.254.0.0/16), skip interfaces psutil reports as down.
No name-based exclusion — users on Windows may legitimately want to
bind a VP to a Hyper-V / WSL / Tailscale virtual adapter.
"""
try:
import psutil
except ImportError:
logger.warning("psutil not available, interface detection unavailable on this platform")
return []
interfaces = []
try:
addrs_by_iface = psutil.net_if_addrs()
stats_by_iface = psutil.net_if_stats()
except Exception as e:
logger.error("psutil failed to enumerate interfaces: %s", e)
return []
for name, addrs in addrs_by_iface.items():
stats = stats_by_iface.get(name)
if stats is not None and not stats.isup:
continue
for addr in addrs:
if addr.family != socket.AF_INET:
continue
ip = addr.address
netmask = addr.netmask
if not ip or not netmask:
continue
try:
ip_obj = ipaddress.IPv4Address(ip)
except ValueError:
continue
if ip_obj.is_loopback or ip_obj.is_link_local:
continue
try:
network = ipaddress.IPv4Network(f"{ip}/{netmask}", strict=False)
except ValueError:
continue
interfaces.append(
{
"name": name,
"ip": ip,
"netmask": netmask,
"subnet": str(network),
}
)
# First IPv4 per interface is enough; matches Linux ioctl which
# returns only the primary IP (aliases land via get_all_interface_ips
# on Linux, which has no Windows analogue worth replicating).
break
return interfaces
def get_network_interfaces() -> list[dict]:
"""Get all network interfaces with their IPs and subnets.
Returns:
List of dicts with name, ip, netmask, subnet, broadcast
"""
# Windows has no fcntl and no `ip` binary; the Linux ioctl path below
# raises ImportError on import fcntl. Route to the psutil-based path
# instead. The Linux path stays as-is for behavioural parity.
if sys.platform == "win32":
return _get_network_interfaces_psutil()
interfaces = []
try: