feat(host/windows): "Punktfunk Host" identity in Task Manager (icon + version info)
punktfunk-host.exe embedded no icon or version resources, so Task Manager and Explorer showed a bare lowercase exe name with a generic icon. build.rs now embeds the branded .ico + FileDescription "Punktfunk Host" / ProductName "Punktfunk" via winresource (same pattern as the Windows client and the tray; Linux packaging builds skip the block). The tray gets a matching "Punktfunk Tray" description, and the SCM display name moves off lowercase "punktfunk streaming host" to "Punktfunk Host" (applied idempotently by `service install` on upgrade). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -243,3 +243,8 @@ nvenc = ["dep:nvidia-video-codec-sdk"]
|
||||
# so the LGPL build suffices and keeps the bundled DLLs LGPL, not GPL) at build time and bundles the
|
||||
# FFmpeg DLLs at runtime. Build the all-vendor GPU host with `--features nvenc,amf-qsv`.
|
||||
amf-qsv = ["dep:ffmpeg-next"]
|
||||
|
||||
# Build-time icon/version-info embedding (build.rs; Windows dev/CI hosts only — Linux packaging
|
||||
# builds of this crate never execute the winresource block).
|
||||
[target.'cfg(windows)'.build-dependencies]
|
||||
winresource = "0.1"
|
||||
|
||||
@@ -17,4 +17,21 @@ fn main() {
|
||||
.unwrap_or_else(|| std::env::var("CARGO_PKG_VERSION").unwrap_or_else(|_| "unknown".into()));
|
||||
println!("cargo:rustc-env=PUNKTFUNK_VERSION={version}");
|
||||
println!("cargo:rerun-if-env-changed=PUNKTFUNK_BUILD_VERSION");
|
||||
|
||||
// Windows identity resources: the branded icon + version info. Task Manager / Explorer show a
|
||||
// process by its version-info FileDescription — without one the host appears as a bare
|
||||
// "punktfunk-host.exe" with no icon. Same winresource pattern as clients/windows and
|
||||
// punktfunk-tray (cfg(windows) = build HOST, so Linux packaging builds skip it; CARGO_CFG_WINDOWS
|
||||
// = TARGET).
|
||||
#[cfg(windows)]
|
||||
if std::env::var_os("CARGO_CFG_WINDOWS").is_some() {
|
||||
let icon = "../../packaging/windows/branding/punktfunk.ico";
|
||||
println!("cargo:rerun-if-changed={icon}");
|
||||
winresource::WindowsResource::new()
|
||||
.set_icon_with_id(icon, "1")
|
||||
.set("FileDescription", "Punktfunk Host")
|
||||
.set("ProductName", "Punktfunk")
|
||||
.compile()
|
||||
.expect("embed windows icon/version resources");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ use windows::Win32::System::Threading::{
|
||||
|
||||
/// SCM service name (the key under HKLM\SYSTEM\CurrentControlSet\Services). Stable identity.
|
||||
const SERVICE_NAME: &str = "PunktfunkHost";
|
||||
const SERVICE_DISPLAY: &str = "punktfunk streaming host";
|
||||
const SERVICE_DISPLAY: &str = "Punktfunk Host";
|
||||
const SERVICE_DESCRIPTION: &str =
|
||||
"Low-latency desktop/game streaming host. Launches the punktfunk host into the active session.";
|
||||
|
||||
@@ -302,6 +302,10 @@ fn run_service() -> Result<()> {
|
||||
.context("set RUNNING")?;
|
||||
tracing::info!("punktfunk service started — supervising host in the active console session");
|
||||
|
||||
// Best-effort: warn if this network is Public (streaming ports are firewalled off there unless
|
||||
// the operator opted in). Own thread — a slow `Get-NetConnectionProfile` never delays the host.
|
||||
std::thread::spawn(warn_if_public_network);
|
||||
|
||||
load_host_env();
|
||||
let result = supervise(stop, session);
|
||||
|
||||
@@ -683,7 +687,14 @@ fn install(args: &[String]) -> Result<()> {
|
||||
if let Some(on) = gamestream {
|
||||
apply_gamestream_choice(on);
|
||||
}
|
||||
add_firewall_rules();
|
||||
// Firewall scope: Domain + Private by default; `--allow-public-network` opts into Public too.
|
||||
// Persist the choice (so the startup warning respects an opt-in) and re-scope idempotently —
|
||||
// remove any prior rules first so an upgrade tightens the scope instead of leaving a stale
|
||||
// all-profiles rule behind the new one.
|
||||
let allow_public = allow_public_network(args);
|
||||
set_fw_public_marker(allow_public);
|
||||
remove_firewall_rules();
|
||||
add_firewall_rules(allow_public);
|
||||
|
||||
println!(
|
||||
"\nInstalled. Config: {}\nLogs: {}\n\nStart now with: punktfunk-host service start",
|
||||
@@ -839,8 +850,28 @@ fn apply_gamestream_choice(enable: bool) {
|
||||
|
||||
// ── firewall + sc helpers ────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// The `netsh` `profile=` scope for punktfunk's inbound rules. Default = **Domain + Private** — the
|
||||
/// trusted-network profiles punktfunk is meant to run on; `allow_public` widens it to **all profiles
|
||||
/// including Public** (untrusted networks like café/hotel Wi-Fi — opt-in only). Shared with the
|
||||
/// web-console rule in `install.rs` so both surfaces scope the same way.
|
||||
pub(crate) fn firewall_profile_arg(allow_public: bool) -> &'static str {
|
||||
if allow_public {
|
||||
"profile=any"
|
||||
} else {
|
||||
"profile=domain,private"
|
||||
}
|
||||
}
|
||||
|
||||
/// The `--allow-public-network` install opt-in (the installer's "Allow connections on Public
|
||||
/// networks" task forwards it). Absent = the secure default (Domain + Private only).
|
||||
pub(crate) fn allow_public_network(args: &[String]) -> bool {
|
||||
args.iter().any(|a| a == "--allow-public-network")
|
||||
}
|
||||
|
||||
/// Inbound firewall rules for the streaming ports (best-effort; logs but never fails the install).
|
||||
fn add_firewall_rules() {
|
||||
/// Scoped by [`firewall_profile_arg`]: Domain + Private by default, all profiles when `allow_public`.
|
||||
fn add_firewall_rules(allow_public: bool) {
|
||||
let profile = firewall_profile_arg(allow_public);
|
||||
// (name suffix, protocol, ports)
|
||||
let rules = [
|
||||
("TCP", "TCP", "47984,47989,48010,47990"),
|
||||
@@ -860,14 +891,22 @@ fn add_firewall_rules() {
|
||||
"action=allow",
|
||||
&format!("protocol={proto}"),
|
||||
&format!("localport={ports}"),
|
||||
profile,
|
||||
],
|
||||
);
|
||||
if ok {
|
||||
println!("Firewall rule added: {name} ({ports})");
|
||||
println!("Firewall rule added: {name} ({ports}) [{profile}]");
|
||||
} else {
|
||||
eprintln!("warning: could not add firewall rule '{name}' (add it manually if needed)");
|
||||
}
|
||||
}
|
||||
if !allow_public {
|
||||
println!(
|
||||
"Note: streaming ports are open on Private/Domain networks only. On a network Windows \
|
||||
classifies as Public, clients won't connect — set that network to Private, or reinstall \
|
||||
with the 'Allow connections on Public networks' option."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_firewall_rules() {
|
||||
@@ -886,6 +925,62 @@ fn remove_firewall_rules() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Marker file recording that the operator opted into opening the firewall on **Public** networks
|
||||
/// (`--allow-public-network`). Its presence suppresses the startup Public-network warning (they made
|
||||
/// an informed choice); absence = the secure default.
|
||||
fn fw_public_marker() -> std::path::PathBuf {
|
||||
crate::gamestream::config_dir().join("fw-allow-public")
|
||||
}
|
||||
|
||||
/// Record (or clear) the Public-firewall opt-in marker to match this install's choice.
|
||||
fn set_fw_public_marker(allow_public: bool) {
|
||||
let path = fw_public_marker();
|
||||
if allow_public {
|
||||
let _ = std::fs::write(&path, b"1\n");
|
||||
} else {
|
||||
let _ = std::fs::remove_file(&path);
|
||||
}
|
||||
}
|
||||
|
||||
/// Best-effort: is any active network connection classified **Public** by Windows? Uses
|
||||
/// `Get-NetConnectionProfile` (per-interface category: Public / Private / DomainAuthenticated).
|
||||
/// `None` when it can't be determined — the caller then skips the warning.
|
||||
fn active_network_is_public() -> Option<bool> {
|
||||
let out = std::process::Command::new("powershell")
|
||||
.args([
|
||||
"-NoProfile",
|
||||
"-NonInteractive",
|
||||
"-Command",
|
||||
"(Get-NetConnectionProfile).NetworkCategory",
|
||||
])
|
||||
.output()
|
||||
.ok()?;
|
||||
if !out.status.success() {
|
||||
return None;
|
||||
}
|
||||
let s = String::from_utf8_lossy(&out.stdout);
|
||||
Some(s.lines().any(|l| l.trim().eq_ignore_ascii_case("Public")))
|
||||
}
|
||||
|
||||
/// One-shot startup diagnostic: if the operator did NOT opt into Public networks and this machine's
|
||||
/// current network is classified Public, the streaming ports are firewalled off there — turn that
|
||||
/// silent "clients can't connect" into an actionable WARN. Best-effort; meant to run on its own
|
||||
/// thread so it never delays the host launch.
|
||||
fn warn_if_public_network() {
|
||||
if fw_public_marker().exists() {
|
||||
return; // operator opted into Public — their informed choice, no warning
|
||||
}
|
||||
if active_network_is_public() == Some(true) {
|
||||
tracing::warn!(
|
||||
"this machine's current network is classified Public (an untrusted-network profile), so \
|
||||
punktfunk's streaming ports are firewalled off here and clients on this network can't \
|
||||
reach the host. Fix: set the network to Private (Windows Settings > Network > \
|
||||
properties) — or, only for a network you trust, reinstall with the 'Allow connections \
|
||||
on Public networks' option."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Run an `sc.exe` command, passing its output through (used by start/stop/status).
|
||||
fn sc(args: &[&str]) -> Result<()> {
|
||||
let status = std::process::Command::new("sc")
|
||||
|
||||
@@ -22,6 +22,9 @@ fn main() {
|
||||
println!("cargo:rerun-if-changed={path}");
|
||||
res.set_icon_with_id(path, id);
|
||||
}
|
||||
// Task Manager / Explorer identity (matches the host's "Punktfunk Host").
|
||||
res.set("FileDescription", "Punktfunk Tray");
|
||||
res.set("ProductName", "Punktfunk");
|
||||
res.compile().expect("embed windows icon resources");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +112,9 @@ SetupIconFile={#BrandingDir}\punktfunk.ico
|
||||
WizardImageFile={#BrandingDir}\wizard-image-*.bmp
|
||||
WizardSmallImageFile={#BrandingDir}\wizard-small-*.bmp
|
||||
UninstallDisplayName=punktfunk host {#MyAppVersion}
|
||||
; The branded multi-size .ico (installed below) - the host exe embeds no icon resource.
|
||||
; The branded multi-size .ico (installed below). The host exe now embeds the same icon + a
|
||||
; "Punktfunk Host" FileDescription (build.rs winresource) for Task Manager/Explorer; the file
|
||||
; copy stays as the uninstall-entry icon.
|
||||
UninstallDisplayIcon={app}\punktfunk.ico
|
||||
|
||||
[Languages]
|
||||
|
||||
Reference in New Issue
Block a user