fix(installer/windows): a silent install can't hang on a prompt, and an upgrade keeps your choices

Three faults that only bite when nobody is watching the wizard — which is how a
package manager always runs us, and how a re-run by hand half-behaves today.

The conflicting-host warning used a plain MsgBox. That ignores /SUPPRESSMSGBOXES
and displays even under /VERYSILENT, so an unattended install onto a box already
running Sunshine/Apollo would sit on a modal dialog nobody can see or click.
SuppressibleMsgBox with an IDNO default aborts instead, which is the honest
answer for a combination the message itself calls unsupported.

The GameStream and Public-firewall choices were re-applied on every run. A silent
run has no wizard, so every task falls back to its script default and the
installer confidently re-asserted it: a user who enabled GameStream had it turned
off again, and one who opened the firewall on Public networks had it quietly
revoked (that task is default-unchecked, so the reset ran the other way). Both
params are now fresh-install-only, keyed off whether host.env already exists.
`service install` already read an absent --gamestream as "keep host.env as-is";
--allow-public-network becomes tri-state the same way, resolving an absent flag
from the marker the previous install recorded. It is strict on the value: a
typo'd =of must not fall through to a marker that may say true, because that
turns a mistyped opt-OUT into leaving Public open.

Also brands the installer "Punktfunk Host" in the strings a user actually sees —
Add/Remove Programs, the Start Menu group, the wizard task text. Paths are
untouched; renaming those would relocate installs and orphan existing config.

Verified on the windows-amd64 runner: ISCC compiles the script (with WithWeb
defined, so the password page parses too), and cargo check + clippy -D warnings
pass for punktfunk-host with nvenc,amf-qsv,qsv.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-26 19:09:13 +02:00
co-authored by Claude Opus 5
parent 1ee06defa6
commit 3d89301336
3 changed files with 92 additions and 27 deletions
+1 -1
View File
@@ -403,7 +403,7 @@ fn web_setup(args: &[String]) -> Result<()> {
// only with `--allow-public-network`. Delete any prior rule first so an upgrade re-scopes it
// instead of stacking a second (possibly all-profiles) rule behind the new one.
let fw_profile =
crate::service::firewall_profile_arg(crate::service::allow_public_network(args));
crate::service::firewall_profile_arg(crate::service::allow_public_network(args)?);
run_quiet(
"netsh",
&[
+34 -6
View File
@@ -718,8 +718,9 @@ fn install(args: &[String]) -> Result<()> {
// 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);
// all-profiles rule behind the new one. With the flag absent (every upgrade) this resolves to
// the previously recorded choice, so the rewrite below is a no-op rather than a silent reset.
let allow_public = allow_public_network(args)?;
set_fw_public_marker(allow_public);
remove_firewall_rules();
add_firewall_rules(allow_public);
@@ -889,10 +890,37 @@ pub(crate) fn firewall_profile_arg(allow_public: bool) -> &'static str {
}
}
/// 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")
/// Resolve the Public-network firewall scope for this install/upgrade (the installer's "Allow
/// connections on Public networks" task forwards the flag).
///
/// Tri-state, mirroring `--gamestream=on|off`:
/// * `--allow-public-network` or `=on` -> `true` — explicit opt-in. The bare form is kept for
/// back-compat with existing scripts and docs that pass it that way.
/// * `--allow-public-network=off` -> `false` — explicit opt-out.
/// * absent -> whatever the previous install recorded.
///
/// The absent case is what makes an UPGRADE non-destructive. A silent upgrade (`winget upgrade`)
/// has no wizard to carry the old checkbox forward, so the installer omits the flag entirely on an
/// upgrade and the recorded choice stands. Re-applying the task default instead would quietly
/// re-scope the firewall on every upgrade, undoing an opt-in the user made once. On a first-ever
/// install there is no marker, so absence resolves to the secure default (Domain + Private only).
///
/// Strict on the value: a typo'd `=of` must NOT fall through to the marker, because the marker may
/// say `true` — i.e. a mistyped opt-OUT would silently leave Public open.
pub(crate) fn allow_public_network(args: &[String]) -> Result<bool> {
for a in args {
if let Some(v) = a.strip_prefix("--allow-public-network") {
return match v {
"" | "=on" => Ok(true),
"=off" => Ok(false),
_ => bail!(
"--allow-public-network must be 'on' or 'off' (got '{}')",
v.trim_start_matches('=')
),
};
}
}
Ok(fw_public_marker().exists())
}
/// Inbound firewall rules for the streaming + mgmt ports (best-effort; logs but never fails the