Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d5a9f66c9 | ||
|
|
5c7a9407ff | ||
|
|
7781d09e26 |
@@ -2652,6 +2652,18 @@ mod pipewire {
|
|||||||
build_default_format_obj(preferred)
|
build_default_format_obj(preferred)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// gamescope trap — the Steam overlay's presence in the stream is decided HERE by omission:
|
||||||
|
// gamescope's `paint_pipewire()` composites the overlay (Shift+Tab / Quick Access Menu) into
|
||||||
|
// the node it hands us ONLY when the consumer-negotiated `gamescope_focus_appid` is 0 — the
|
||||||
|
// default, and the "mirror the focused window + overlay" branch (gamescope ≥ 3.16.23; see
|
||||||
|
// `MIN_GAMESCOPE_OVERLAY`). None of the EnumFormat pods below advertise the
|
||||||
|
// `SPA_FORMAT_VIDEO_gamescope_focus_appid` property, so gamescope reads 0 and paints the
|
||||||
|
// overlay for us for free. DO NOT add a non-zero focus-appid (e.g. to "isolate the game" in a
|
||||||
|
// dedicated session) — that flips gamescope into the Remote-Play branch that deliberately
|
||||||
|
// drops the overlay (and all host chrome) back out of the capture. The cursor, external
|
||||||
|
// overlay (MangoHUD), and notifications are excluded from the node on EVERY gamescope
|
||||||
|
// version and are composited host-side instead (see `xfixes_cursor.rs`).
|
||||||
|
//
|
||||||
// When zero-copy is on, offer ONLY a BGRx dmabuf format with our EGL-importable modifiers
|
// When zero-copy is on, offer ONLY a BGRx dmabuf format with our EGL-importable modifiers
|
||||||
// (offering shm too makes the compositor pick shm). The modifier list is advertised with
|
// (offering shm too makes the compositor pick shm). The modifier list is advertised with
|
||||||
// DONT_FIXATE so the compositor's allocator chooses one; we re-emit the fixated format in
|
// DONT_FIXATE so the compositor's allocator chooses one; we re-emit the fixated format in
|
||||||
|
|||||||
@@ -1139,18 +1139,67 @@ fn dm_survives_masked_unit(dm: &str) -> bool {
|
|||||||
dm == "sddm.service"
|
dm == "sddm.service"
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stop the display manager for a takeover on a mask-fragile DM flavor. Plain `systemctl stop` on
|
/// The packaged privileged fallback for the display-manager takeover verbs: a root helper behind
|
||||||
/// the SYSTEM bus — succeeds as root or under an operator polkit rule scoped to the DM unit (see
|
/// its own polkit action (`io.unom.punktfunk.dm-helper`, `allow_any` — the mechanism these
|
||||||
/// docs); fails cleanly otherwise ("interactive authentication required"), in which case the
|
/// distros use for their own session switcher, e.g. Nobara's `os-session-select`), so the managed
|
||||||
/// caller degrades to attach.
|
/// takeover works out of the box on mask-fragile DM flavors with no hand-installed polkit rule.
|
||||||
fn try_stop_display_manager(dm: &str) -> bool {
|
/// The helper derives the DM unit from the `display-manager.service` symlink itself, so this
|
||||||
Command::new("systemctl")
|
/// process never gets to name an arbitrary unit across the privilege boundary. Two layouts: the
|
||||||
.args(["stop", dm])
|
/// rpm/deb `libexec` path (what the shipped policy annotates) and Arch's `/usr/lib/<pkg>` (its
|
||||||
|
/// PKGBUILD rewrites the annotation to match).
|
||||||
|
const DM_HELPER_PATHS: &[&str] = &[
|
||||||
|
"/usr/libexec/punktfunk/pf-dm-helper",
|
||||||
|
"/usr/lib/punktfunk/pf-dm-helper",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Run the packaged DM helper (`stop` | `restore`) via pkexec. `false` when the helper isn't
|
||||||
|
/// installed (tarball/old package), pkexec is missing, or polkit denies the action.
|
||||||
|
fn dm_helper(verb: &str) -> bool {
|
||||||
|
let Some(helper) = DM_HELPER_PATHS
|
||||||
|
.iter()
|
||||||
|
.find(|p| std::path::Path::new(p).exists())
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
Command::new("pkexec")
|
||||||
|
.arg(helper)
|
||||||
|
.arg(verb)
|
||||||
.status()
|
.status()
|
||||||
.map(|s| s.success())
|
.map(|s| s.success())
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Stop the display manager for a takeover on a mask-fragile DM flavor. Plain `systemctl stop` on
|
||||||
|
/// the SYSTEM bus first — succeeds as root or under an operator polkit rule scoped to the DM unit
|
||||||
|
/// (see docs); fails cleanly otherwise ("interactive authentication required") — then the
|
||||||
|
/// packaged pkexec helper. `false` means no privilege path exists and the caller degrades to
|
||||||
|
/// attach.
|
||||||
|
fn try_stop_display_manager(dm: &str) -> bool {
|
||||||
|
let direct = Command::new("systemctl")
|
||||||
|
.args(["stop", dm])
|
||||||
|
.status()
|
||||||
|
.map(|s| s.success())
|
||||||
|
.unwrap_or(false);
|
||||||
|
direct || dm_helper("stop")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Restore the display manager: `reset-failed` (a relogin loop may have tripped the unit's start
|
||||||
|
/// limit, and a plain restart is refused until the accounting clears) + `restart` — its autologin
|
||||||
|
/// session Exec brings the box's own session back up. Plain system-bus verbs first (root / an
|
||||||
|
/// operator polkit rule), then the packaged pkexec helper, whose `restore` verb performs the same
|
||||||
|
/// two steps as root.
|
||||||
|
fn restore_display_manager(dm: &str) -> bool {
|
||||||
|
let _ = Command::new("systemctl")
|
||||||
|
.args(["reset-failed", dm])
|
||||||
|
.status();
|
||||||
|
let direct = Command::new("systemctl")
|
||||||
|
.args(["restart", dm])
|
||||||
|
.status()
|
||||||
|
.map(|s| s.success())
|
||||||
|
.unwrap_or(false);
|
||||||
|
direct || dm_helper("restore")
|
||||||
|
}
|
||||||
|
|
||||||
/// The distro's session-switch helper (ChimeraOS/Nobara layout). Its USER pass records the
|
/// The distro's session-switch helper (ChimeraOS/Nobara layout). Its USER pass records the
|
||||||
/// sentinel + self-pkexecs (authorized `allow_any` by the distro's own polkit action policy, so
|
/// sentinel + self-pkexecs (authorized `allow_any` by the distro's own polkit action policy, so
|
||||||
/// it works from our sessionless context); its ROOT pass rewrites the DM autologin config — but
|
/// it works from our sessionless context); its ROOT pass rewrites the DM autologin config — but
|
||||||
@@ -1219,10 +1268,13 @@ fn honor_session_select_switch(dm: String) {
|
|||||||
clear_takeover();
|
clear_takeover();
|
||||||
*MANAGED_SESSION.lock().unwrap_or_else(|e| e.into_inner()) = None;
|
*MANAGED_SESSION.lock().unwrap_or_else(|e| e.into_inner()) = None;
|
||||||
stop_session(SESSION_UNIT); // dead already (the switch shut its Steam down) — clear the unit
|
stop_session(SESSION_UNIT); // dead already (the switch shut its Steam down) — clear the unit
|
||||||
let _ = Command::new("systemctl")
|
if !restore_display_manager(&dm) {
|
||||||
.args(["reset-failed", &dm])
|
tracing::warn!(
|
||||||
.status();
|
%dm,
|
||||||
let _ = Command::new("systemctl").args(["start", &dm]).status();
|
"gamescope: display-manager start was denied — the desktop switch may need a manual \
|
||||||
|
`systemctl restart` of the DM"
|
||||||
|
);
|
||||||
|
}
|
||||||
let deadline = Instant::now() + Duration::from_secs(10);
|
let deadline = Instant::now() + Duration::from_secs(10);
|
||||||
while Instant::now() < deadline {
|
while Instant::now() < deadline {
|
||||||
let active = Command::new("systemctl")
|
let active = Command::new("systemctl")
|
||||||
@@ -1343,8 +1395,10 @@ fn stop_autologin_sessions() -> Result<()> {
|
|||||||
if !try_stop_display_manager(&dm) {
|
if !try_stop_display_manager(&dm) {
|
||||||
bail!(
|
bail!(
|
||||||
"the box's gaming session is driven by {dm}, which does not survive a masked \
|
"the box's gaming session is driven by {dm}, which does not survive a masked \
|
||||||
session unit, and stopping it needs privilege — install the punktfunk \
|
session unit, and stopping it needs privilege — the packaged pf-dm-helper \
|
||||||
display-manager polkit rule (see docs) to enable the managed takeover"
|
polkit action is missing or was denied (reinstall the punktfunk package, or \
|
||||||
|
install the display-manager polkit rule from the docs) so the managed takeover \
|
||||||
|
is unavailable"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
@@ -1668,14 +1722,7 @@ fn do_restore_tv_session() {
|
|||||||
// (`reset-failed` clears the relogin start-limit accounting, then `restart`); its autologin
|
// (`reset-failed` clears the relogin start-limit accounting, then `restart`); its autologin
|
||||||
// session Exec starts the gamescope unit itself.
|
// session Exec starts the gamescope unit itself.
|
||||||
if let Some(dm) = dm {
|
if let Some(dm) = dm {
|
||||||
let _ = Command::new("systemctl")
|
let restart = restore_display_manager(&dm);
|
||||||
.args(["reset-failed", &dm])
|
|
||||||
.status();
|
|
||||||
let restart = Command::new("systemctl")
|
|
||||||
.args(["restart", &dm])
|
|
||||||
.status()
|
|
||||||
.map(|s| s.success())
|
|
||||||
.unwrap_or(false);
|
|
||||||
if restart {
|
if restart {
|
||||||
tracing::info!(%dm, "restored the display manager (its autologin brings gaming mode back)");
|
tracing::info!(%dm, "restored the display manager (its autologin brings gaming mode back)");
|
||||||
} else if crate::try_recover_session() {
|
} else if crate::try_recover_session() {
|
||||||
|
|||||||
@@ -340,9 +340,19 @@ pub(crate) fn is_available() -> bool {
|
|||||||
/// against PipeWire ≥ 1.6 (a loop-lock bug) and a stuck link head-blocks the whole daemon.
|
/// against PipeWire ≥ 1.6 (a loop-lock bug) and a stuck link head-blocks the whole daemon.
|
||||||
const MIN_GAMESCOPE: (u32, u32, u32) = (3, 16, 22);
|
const MIN_GAMESCOPE: (u32, u32, u32) = (3, 16, 22);
|
||||||
|
|
||||||
/// Best-effort: warn loudly if the installed gamescope is older than [`MIN_GAMESCOPE`]. Parsing
|
/// First gamescope that paints the Steam overlay (Shift+Tab / Quick Access Menu) into its built-in
|
||||||
/// failures are silent (don't block a possibly-fine custom build) — this is a diagnostic, not a
|
/// PipeWire node. `paint_pipewire()` is a *separate, reduced* composite from the display scanout;
|
||||||
/// gate. Returns the parsed version when it could read one.
|
/// the overlay-window paint (gated on the consumer negotiating `gamescope_focus_appid == 0`, which
|
||||||
|
/// we do by never advertising that property — see the capturer's EnumFormat builders) first ships
|
||||||
|
/// in 3.16.23 (gamescope commits `ccd62074` + `f8b33d38`). Below this the overlay is *never* in the
|
||||||
|
/// node, so it cannot appear in the stream no matter what the host does. The cursor and
|
||||||
|
/// external-overlay / notification layers are excluded on *every* version (handled host-side).
|
||||||
|
const MIN_GAMESCOPE_OVERLAY: (u32, u32, u32) = (3, 16, 23);
|
||||||
|
|
||||||
|
/// Best-effort: warn if the installed gamescope is older than [`MIN_GAMESCOPE`] (capture is
|
||||||
|
/// unreliable) or than [`MIN_GAMESCOPE_OVERLAY`] (capture works but the Steam overlay can't reach
|
||||||
|
/// the stream). Parsing failures are silent (don't block a possibly-fine custom build) — this is a
|
||||||
|
/// diagnostic, not a gate. Returns the parsed version when it could read one.
|
||||||
pub(super) fn check_gamescope_version() -> Option<(u32, u32, u32)> {
|
pub(super) fn check_gamescope_version() -> Option<(u32, u32, u32)> {
|
||||||
let out = Command::new("gamescope").arg("--version").output().ok()?;
|
let out = Command::new("gamescope").arg("--version").output().ok()?;
|
||||||
// gamescope prints the version banner to stderr on some builds, stdout on others.
|
// gamescope prints the version banner to stderr on some builds, stdout on others.
|
||||||
@@ -360,6 +370,18 @@ pub(super) fn check_gamescope_version() -> Option<(u32, u32, u32)> {
|
|||||||
capture deadlock against PipeWire ≥ 1.6 (a wedged link head-blocks the daemon); \
|
capture deadlock against PipeWire ≥ 1.6 (a wedged link head-blocks the daemon); \
|
||||||
upgrade gamescope or use PUNKTFUNK_COMPOSITOR=kwin|mutter"
|
upgrade gamescope or use PUNKTFUNK_COMPOSITOR=kwin|mutter"
|
||||||
);
|
);
|
||||||
|
} else if ver < MIN_GAMESCOPE_OVERLAY {
|
||||||
|
// Capture is fine; the Steam overlay just won't be in the frame gamescope hands us.
|
||||||
|
tracing::warn!(
|
||||||
|
found = %format!("{}.{}.{}", ver.0, ver.1, ver.2),
|
||||||
|
min = %format!(
|
||||||
|
"{}.{}.{}",
|
||||||
|
MIN_GAMESCOPE_OVERLAY.0, MIN_GAMESCOPE_OVERLAY.1, MIN_GAMESCOPE_OVERLAY.2
|
||||||
|
),
|
||||||
|
"gamescope is older than the first version that paints the Steam overlay (Shift+Tab / \
|
||||||
|
Quick Access Menu) into its PipeWire node — the overlay will be absent from the \
|
||||||
|
stream until you upgrade gamescope (the cursor is composited host-side regardless)"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
Some(ver)
|
Some(ver)
|
||||||
}
|
}
|
||||||
@@ -379,7 +401,7 @@ fn parse_version(text: &str) -> Option<(u32, u32, u32)> {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{parse_version, steam_appid_from_launch, MIN_GAMESCOPE};
|
use super::{parse_version, steam_appid_from_launch, MIN_GAMESCOPE, MIN_GAMESCOPE_OVERLAY};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parses_steam_appid_from_launch() {
|
fn parses_steam_appid_from_launch() {
|
||||||
@@ -425,4 +447,15 @@ mod tests {
|
|||||||
assert!(parse_version("gamescope version 3.16.22").unwrap() >= MIN_GAMESCOPE);
|
assert!(parse_version("gamescope version 3.16.22").unwrap() >= MIN_GAMESCOPE);
|
||||||
assert!(parse_version("gamescope version 3.17.0").unwrap() >= MIN_GAMESCOPE);
|
assert!(parse_version("gamescope version 3.17.0").unwrap() >= MIN_GAMESCOPE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn overlay_threshold_brackets_the_fix() {
|
||||||
|
// 3.16.22 captures fine but predates the overlay-in-pipewire paint — it sits in the
|
||||||
|
// "capture works, overlay absent" window `[MIN_GAMESCOPE, MIN_GAMESCOPE_OVERLAY)`, which is
|
||||||
|
// exactly the `else if` warn arm; 3.16.23 is the first to include the overlay.
|
||||||
|
assert!(parse_version("gamescope version 3.16.22").unwrap() >= MIN_GAMESCOPE);
|
||||||
|
assert!(parse_version("gamescope version 3.16.22").unwrap() < MIN_GAMESCOPE_OVERLAY);
|
||||||
|
assert!(parse_version("gamescope version 3.16.23").unwrap() >= MIN_GAMESCOPE_OVERLAY);
|
||||||
|
assert!(parse_version("gamescope version 3.16.25").unwrap() >= MIN_GAMESCOPE_OVERLAY);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -431,16 +431,69 @@ fn web_setup(args: &[String]) -> Result<()> {
|
|||||||
) {
|
) {
|
||||||
eprintln!("warning: could not add the firewall rule for TCP 47992");
|
eprintln!("warning: could not add the firewall rule for TCP 47992");
|
||||||
}
|
}
|
||||||
// 5. wait briefly for the host's mgmt token, then start (restart-on-failure picks it up otherwise)
|
// 5. Wait for EVERY file the launcher needs, then start the task and VERIFY it came up.
|
||||||
for _ in 0..30 {
|
//
|
||||||
if token_path.exists() {
|
// `web-run.cmd` refuses to serve without the mgmt token AND the host identity cert, and the
|
||||||
break;
|
// host writes them in that order: the token during argument parsing (`main::parse_serve`,
|
||||||
|
// milliseconds after `serve` starts), the cert only once `serve` reaches
|
||||||
|
// `gamestream::cert::ServerIdentity::load_or_create` - behind a pure-Rust RSA-2048 keygen
|
||||||
|
// whose prime search has a multi-second tail. Gating on the token alone therefore fired
|
||||||
|
// `schtasks /run` while that keygen was still running: the launcher exited 1, and since the
|
||||||
|
// task carries no trigger other than boot (and Task Scheduler's restart-on-failure does not
|
||||||
|
// reliably fire for a plain non-zero exit code), a freshly installed console stayed down
|
||||||
|
// until the next reboot. Gate on `cert.pem` - written LAST, after `key.pem` - so all three
|
||||||
|
// files are on disk before the first start.
|
||||||
|
let cert_path = data_dir.join("cert.pem");
|
||||||
|
if !wait_for_files(&[token_path.as_path(), cert_path.as_path()], 90) {
|
||||||
|
eprintln!(
|
||||||
|
"warning: the host service has not written {} + {} yet - not starting the console now; \
|
||||||
|
it will start at the next boot (or run: schtasks /run /tn {WEB_TASK})",
|
||||||
|
token_path.display(),
|
||||||
|
cert_path.display()
|
||||||
|
);
|
||||||
|
println!("web console set up (https://<host-ip>:47992)");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if start_web_task() {
|
||||||
|
println!("web console set up + started (https://<host-ip>:47992)");
|
||||||
|
} else {
|
||||||
|
// Never claim "started" when it isn't: the launcher's own diagnostics go to a task with no
|
||||||
|
// console, so this warning is the only trace an operator gets at install time.
|
||||||
|
eprintln!(
|
||||||
|
"warning: the {WEB_TASK} task did not bring up a listener on :47992 - check its Last Run \
|
||||||
|
Result in Task Scheduler; the console will be retried at the next boot"
|
||||||
|
);
|
||||||
|
println!("web console set up (https://<host-ip>:47992)");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Poll until every path exists, or `secs` elapse. Returns whether they all showed up.
|
||||||
|
fn wait_for_files(paths: &[&Path], secs: u64) -> bool {
|
||||||
|
for _ in 0..secs {
|
||||||
|
if paths.iter().all(|p| p.exists()) {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||||
}
|
}
|
||||||
run_quiet("schtasks", &["/run", "/tn", WEB_TASK]);
|
paths.iter().all(|p| p.exists())
|
||||||
println!("web console set up + started (https://<host-ip>:47992)");
|
}
|
||||||
Ok(())
|
|
||||||
|
/// `schtasks /run` + verify: the command reports only that a start was *attempted*, so poll for the
|
||||||
|
/// console's own listener and retry a couple of times before giving up. Returns whether :47992 ended
|
||||||
|
/// up listening.
|
||||||
|
fn start_web_task() -> bool {
|
||||||
|
for _ in 0..3 {
|
||||||
|
run_quiet("schtasks", &["/run", "/tn", WEB_TASK]);
|
||||||
|
// Bun binds the port a second or two after launch on a warm box; allow for a cold one.
|
||||||
|
for _ in 0..10 {
|
||||||
|
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||||
|
if !web_listener_pids().is_empty() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Source: a non-empty `--password-file` (fresh install) > keep existing (upgrade) > random fallback.
|
/// Source: a non-empty `--password-file` (fresh install) > keep existing (upgrade) > random fallback.
|
||||||
@@ -505,30 +558,57 @@ fn random_password() -> String {
|
|||||||
/// ("LISTENING"/"ABHOEREN"/...) is never parsed.
|
/// ("LISTENING"/"ABHOEREN"/...) is never parsed.
|
||||||
fn stop_web_console() {
|
fn stop_web_console() {
|
||||||
run_quiet("schtasks", &["/end", "/tn", WEB_TASK]);
|
run_quiet("schtasks", &["/end", "/tn", WEB_TASK]);
|
||||||
for line in run_capture("netstat", &["-ano", "-p", "tcp"]).lines() {
|
for pid in web_listener_pids() {
|
||||||
let toks: Vec<&str> = line.split_whitespace().collect();
|
run_quiet("taskkill", &["/PID", &pid, "/F"]);
|
||||||
if toks.len() >= 5
|
|
||||||
&& toks[0].eq_ignore_ascii_case("tcp")
|
|
||||||
&& toks[1].ends_with(":47992")
|
|
||||||
&& (toks[2] == "0.0.0.0:0" || toks[2] == "[::]:0")
|
|
||||||
{
|
|
||||||
let pid = toks[toks.len() - 1];
|
|
||||||
if !pid.is_empty() && pid.bytes().all(|b| b.is_ascii_digit()) {
|
|
||||||
run_quiet("taskkill", &["/PID", pid, "/F"]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Register the boot/SYSTEM/restart-on-failure task via a generated Task Scheduler XML (`schtasks /xml`,
|
/// PIDs owning a LISTEN socket on :47992. Also the console's liveness probe (`start_web_task`).
|
||||||
/// no COM). The XML declares UTF-16, so it's written UTF-16LE+BOM.
|
fn web_listener_pids() -> Vec<String> {
|
||||||
|
run_capture("netstat", &["-ano", "-p", "tcp"])
|
||||||
|
.lines()
|
||||||
|
.filter_map(|line| {
|
||||||
|
let toks: Vec<&str> = line.split_whitespace().collect();
|
||||||
|
let listening = toks.len() >= 5
|
||||||
|
&& toks[0].eq_ignore_ascii_case("tcp")
|
||||||
|
&& toks[1].ends_with(":47992")
|
||||||
|
&& (toks[2] == "0.0.0.0:0" || toks[2] == "[::]:0");
|
||||||
|
let pid = *toks.last()?;
|
||||||
|
(listening && !pid.is_empty() && pid.bytes().all(|b| b.is_ascii_digit()))
|
||||||
|
.then(|| pid.to_string())
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Register the boot+logon/SYSTEM/restart-on-failure task via a generated Task Scheduler XML
|
||||||
|
/// (`schtasks /xml`, no COM). The XML declares UTF-16, so it's written UTF-16LE+BOM.
|
||||||
|
///
|
||||||
|
/// Two triggers, because boot alone left too many holes: a console that lost the install-time start
|
||||||
|
/// (or a box where the host service was started only later) had to wait for a full reboot. Logon is
|
||||||
|
/// free insurance - `MultipleInstancesPolicy=IgnoreNew` makes a redundant start a no-op. If a
|
||||||
|
/// Task Scheduler build rejects the two-trigger XML we fall back to the boot-only form rather than
|
||||||
|
/// fail registration outright (no task at all is strictly worse than a boot-only task).
|
||||||
fn register_web_task(cmd: &Path) -> Result<()> {
|
fn register_web_task(cmd: &Path) -> Result<()> {
|
||||||
|
let cmd_xml = xml_escape(&cmd.to_string_lossy());
|
||||||
|
let boot = "<BootTrigger><Enabled>true</Enabled></BootTrigger>";
|
||||||
|
let boot_and_logon = "<BootTrigger><Enabled>true</Enabled></BootTrigger>\
|
||||||
|
<LogonTrigger><Enabled>true</Enabled></LogonTrigger>";
|
||||||
|
if try_register_web_task(&cmd_xml, boot_and_logon) || try_register_web_task(&cmd_xml, boot) {
|
||||||
|
println!("registered scheduled task {WEB_TASK} -> {}", cmd.display());
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
bail!("schtasks /create {WEB_TASK} failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One `schtasks /create /xml` attempt with the given `<Triggers>` body. Returns whether it took.
|
||||||
|
fn try_register_web_task(cmd_xml: &str, triggers: &str) -> bool {
|
||||||
let xml = format!(
|
let xml = format!(
|
||||||
"<?xml version=\"1.0\" encoding=\"UTF-16\"?>\n\
|
"<?xml version=\"1.0\" encoding=\"UTF-16\"?>\n\
|
||||||
<Task version=\"1.2\" xmlns=\"http://schemas.microsoft.com/windows/2004/02/mit/task\">\n\
|
<Task version=\"1.2\" xmlns=\"http://schemas.microsoft.com/windows/2004/02/mit/task\">\n\
|
||||||
<RegistrationInfo><Description>punktfunk web management console (Nitro SSR on bun, :47992)</Description></RegistrationInfo>\n\
|
<RegistrationInfo><Description>punktfunk web management console (Nitro SSR on bun, :47992)</Description></RegistrationInfo>\n\
|
||||||
<Triggers><BootTrigger><Enabled>true</Enabled></BootTrigger></Triggers>\n\
|
<Triggers>{triggers}</Triggers>\n\
|
||||||
<Principals><Principal id=\"Author\"><UserId>S-1-5-18</UserId><RunLevel>HighestAvailable</RunLevel></Principal></Principals>\n\
|
<Principals><Principal id=\"Author\"><UserId>S-1-5-18</UserId><RunLevel>HighestAvailable</RunLevel></Principal></Principals>\n\
|
||||||
<Settings>\n\
|
<Settings>\n\
|
||||||
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>\n\
|
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>\n\
|
||||||
@@ -538,12 +618,13 @@ fn register_web_task(cmd: &Path) -> Result<()> {
|
|||||||
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>\n\
|
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>\n\
|
||||||
<RestartOnFailure><Interval>PT1M</Interval><Count>10</Count></RestartOnFailure>\n\
|
<RestartOnFailure><Interval>PT1M</Interval><Count>10</Count></RestartOnFailure>\n\
|
||||||
</Settings>\n\
|
</Settings>\n\
|
||||||
<Actions Context=\"Author\"><Exec><Command>{}</Command></Exec></Actions>\n\
|
<Actions Context=\"Author\"><Exec><Command>{cmd_xml}</Command></Exec></Actions>\n\
|
||||||
</Task>",
|
</Task>"
|
||||||
xml_escape(&cmd.to_string_lossy())
|
|
||||||
);
|
);
|
||||||
let xml_path = std::env::temp_dir().join("punktfunk-web-task.xml");
|
let xml_path = std::env::temp_dir().join("punktfunk-web-task.xml");
|
||||||
write_utf16le_bom(&xml_path, &xml)?;
|
if write_utf16le_bom(&xml_path, &xml).is_err() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
let ok = run_quiet(
|
let ok = run_quiet(
|
||||||
"schtasks",
|
"schtasks",
|
||||||
&[
|
&[
|
||||||
@@ -556,12 +637,7 @@ fn register_web_task(cmd: &Path) -> Result<()> {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
let _ = std::fs::remove_file(&xml_path);
|
let _ = std::fs::remove_file(&xml_path);
|
||||||
if ok {
|
ok
|
||||||
println!("registered scheduled task {WEB_TASK} -> {}", cmd.display());
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
bail!("schtasks /create {WEB_TASK} failed")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_utf16le_bom(path: &Path, s: &str) -> Result<()> {
|
fn write_utf16le_bom(path: &Path, s: &str) -> Result<()> {
|
||||||
|
|||||||
@@ -40,8 +40,16 @@ depends on the display manager driving the autologin:
|
|||||||
|
|
||||||
- **SDDM** (Bazzite, SteamOS): handled automatically — no setup.
|
- **SDDM** (Bazzite, SteamOS): handled automatically — no setup.
|
||||||
- **plasmalogin** (Nobara) and other display managers: the host must stop the display manager
|
- **plasmalogin** (Nobara) and other display managers: the host must stop the display manager
|
||||||
itself for the length of the stream and restart it afterwards, which needs privilege. Allow it
|
itself for the length of the stream and restart it afterwards, which needs privilege. The
|
||||||
with a polkit rule (adjust the unit and user names to your box):
|
packages ship that privilege: a root helper (`/usr/libexec/punktfunk/pf-dm-helper`) behind its
|
||||||
|
own polkit action (`io.unom.punktfunk.dm-helper`), invoked automatically when the plain
|
||||||
|
`systemctl` verbs are denied — no setup. The helper only stops/restores the unit the
|
||||||
|
`display-manager.service` symlink points at, the same class of local-seat operation these
|
||||||
|
distros already authorize for their own session switcher (Nobara's `os-session-select`).
|
||||||
|
|
||||||
|
Installed from a tarball, or prefer not to ship the `allow_any` action? Remove the `.policy`
|
||||||
|
file and use a polkit rule scoped to your user instead (adjust the unit and user names to your
|
||||||
|
box) — the host tries the plain verbs first, so the rule takes precedence:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// /etc/polkit-1/rules.d/49-punktfunk-dm.rules
|
// /etc/polkit-1/rules.d/49-punktfunk-dm.rules
|
||||||
@@ -54,15 +62,16 @@ depends on the display manager driving the autologin:
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
Without the rule the host degrades safely: it **attaches** to the live Gaming Mode session
|
With no privilege path at all the host degrades safely: it **attaches** to the live Gaming Mode
|
||||||
instead (Game Mode stays on the box's display, mirrored to the client) rather than risk the
|
session instead (Game Mode stays on the box's display at the box's own resolution, mirrored to
|
||||||
display manager. If the display-manager restart ever loses its privilege mid-restore,
|
the client — if your monitor stays on and the stream runs at the desktop's resolution, this is
|
||||||
`PUNKTFUNK_RECOVER_SESSION_CMD` (see [Configuration](/docs/configuration)) is fired as the
|
what happened; check the host log for "managed takeover unavailable"). If the display-manager
|
||||||
fallback.
|
restart ever loses its privilege mid-restore, `PUNKTFUNK_RECOVER_SESSION_CMD` (see
|
||||||
|
[Configuration](/docs/configuration)) is fired as the fallback.
|
||||||
|
|
||||||
With the rule in place the **in-stream session switch round-trips** in managed mode: Steam's
|
With the takeover authorized the **in-stream session switch round-trips** in managed mode:
|
||||||
"Switch to Desktop" inside the streamed Game Mode returns the box to its desktop session and the
|
Steam's "Switch to Desktop" inside the streamed Game Mode returns the box to its desktop session
|
||||||
stream follows it there; the desktop's "Return to Gaming Mode" switches it forward again.
|
and the stream follows it there; the desktop's "Return to Gaming Mode" switches it forward again.
|
||||||
|
|
||||||
## Session following
|
## Session following
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ the Windows specifics follow.
|
|||||||
|
|
||||||
The installer also sets up the **web management console** (status, paired devices, the PIN pairing
|
The installer also sets up the **web management console** (status, paired devices, the PIN pairing
|
||||||
flow): it bundles the console plus its own runtime and runs it as the **`PunktfunkWeb`** task on
|
flow): it bundles the console plus its own runtime and runs it as the **`PunktfunkWeb`** task on
|
||||||
**`https://<this-PC>:47992`**, starting at boot.
|
**`https://<this-PC>:47992`**, starting at boot and at sign-in.
|
||||||
|
|
||||||
#### Console login password
|
#### Console login password
|
||||||
|
|
||||||
|
|||||||
@@ -129,6 +129,14 @@ package_punktfunk-host() {
|
|||||||
install -Dm0755 "$T/punktfunk-host" "$pkgdir/usr/bin/punktfunk-host"
|
install -Dm0755 "$T/punktfunk-host" "$pkgdir/usr/bin/punktfunk-host"
|
||||||
# /dev/uinput + /dev/uhid -> input group (virtual gamepads + DualSense UHID)
|
# /dev/uinput + /dev/uhid -> input group (virtual gamepads + DualSense UHID)
|
||||||
install -Dm0644 "$R/scripts/60-punktfunk.rules" "$pkgdir/usr/lib/udev/rules.d/60-punktfunk.rules"
|
install -Dm0644 "$R/scripts/60-punktfunk.rules" "$pkgdir/usr/lib/udev/rules.d/60-punktfunk.rules"
|
||||||
|
# Managed gamescope takeover on DM-autologin boxes: root helper + polkit action so the host can
|
||||||
|
# stop/restore the display manager for the stream. Arch has no /usr/libexec — install under
|
||||||
|
# /usr/lib/punktfunk and rewrite the policy's exec.path annotation to match (the host probes both).
|
||||||
|
install -Dm0755 "$R/scripts/pf-dm-helper" "$pkgdir/usr/lib/punktfunk/pf-dm-helper"
|
||||||
|
install -Dm0644 "$R/scripts/io.unom.punktfunk.dm-helper.policy" \
|
||||||
|
"$pkgdir/usr/share/polkit-1/actions/io.unom.punktfunk.dm-helper.policy"
|
||||||
|
sed -i 's#/usr/libexec/punktfunk/pf-dm-helper#/usr/lib/punktfunk/pf-dm-helper#' \
|
||||||
|
"$pkgdir/usr/share/polkit-1/actions/io.unom.punktfunk.dm-helper.policy"
|
||||||
# vhci-hcd autoload — usbip transport for the virtual Steam Deck pad (Steam only adopts USB pads)
|
# vhci-hcd autoload — usbip transport for the virtual Steam Deck pad (Steam only adopts USB pads)
|
||||||
install -Dm0644 "$R/scripts/punktfunk-modules.conf" "$pkgdir/usr/lib/modules-load.d/punktfunk.conf"
|
install -Dm0644 "$R/scripts/punktfunk-modules.conf" "$pkgdir/usr/lib/modules-load.d/punktfunk.conf"
|
||||||
# 32 MB UDP socket buffers (send-side headroom at high bitrate)
|
# 32 MB UDP socket buffers (send-side headroom at high bitrate)
|
||||||
|
|||||||
@@ -52,6 +52,11 @@ SHAREDIR="$STAGE/usr/share/$PKG"
|
|||||||
# --- file layout (matches the RPM %install) ----------------------------------
|
# --- file layout (matches the RPM %install) ----------------------------------
|
||||||
install -Dm0755 "$BIN" "$STAGE/usr/bin/$PKG"
|
install -Dm0755 "$BIN" "$STAGE/usr/bin/$PKG"
|
||||||
install -Dm0644 scripts/60-punktfunk.rules "$STAGE/usr/lib/udev/rules.d/60-punktfunk.rules"
|
install -Dm0644 scripts/60-punktfunk.rules "$STAGE/usr/lib/udev/rules.d/60-punktfunk.rules"
|
||||||
|
# Managed gamescope takeover on DM-autologin boxes: root helper + polkit action so the host can
|
||||||
|
# stop/restore the display manager for the stream (the helper derives the DM unit itself).
|
||||||
|
install -Dm0755 scripts/pf-dm-helper "$STAGE/usr/libexec/punktfunk/pf-dm-helper"
|
||||||
|
install -Dm0644 scripts/io.unom.punktfunk.dm-helper.policy \
|
||||||
|
"$STAGE/usr/share/polkit-1/actions/io.unom.punktfunk.dm-helper.policy"
|
||||||
# vhci-hcd autoload — usbip transport for the virtual Steam Deck pad (Steam only adopts USB pads).
|
# vhci-hcd autoload — usbip transport for the virtual Steam Deck pad (Steam only adopts USB pads).
|
||||||
install -Dm0644 scripts/punktfunk-modules.conf "$STAGE/usr/lib/modules-load.d/punktfunk.conf"
|
install -Dm0644 scripts/punktfunk-modules.conf "$STAGE/usr/lib/modules-load.d/punktfunk.conf"
|
||||||
# UDP socket-buffer tuning (32 MB) — without it the kernel clamps the host's SO_SNDBUF to ~416 KB
|
# UDP socket-buffer tuning (32 MB) — without it the kernel clamps the host's SO_SNDBUF to ~416 KB
|
||||||
|
|||||||
@@ -249,6 +249,12 @@ install -Dm0755 target/release/punktfunk-host %{buildroot}%{_bindir}/punktfunk-h
|
|||||||
# udev rule — /dev/uinput access for virtual gamepads (input group).
|
# udev rule — /dev/uinput access for virtual gamepads (input group).
|
||||||
install -Dm0644 scripts/60-punktfunk.rules %{buildroot}%{_udevrulesdir}/60-punktfunk.rules
|
install -Dm0644 scripts/60-punktfunk.rules %{buildroot}%{_udevrulesdir}/60-punktfunk.rules
|
||||||
|
|
||||||
|
# Managed gamescope takeover on DM-autologin boxes (Nobara's plasmalogin): a root helper + polkit
|
||||||
|
# action let the host stop/restore the display manager for the stream without a hand-installed
|
||||||
|
# polkit rule. The helper derives the DM unit itself — callers can't name arbitrary units.
|
||||||
|
install -Dm0755 scripts/pf-dm-helper %{buildroot}%{_libexecdir}/punktfunk/pf-dm-helper
|
||||||
|
install -Dm0644 scripts/io.unom.punktfunk.dm-helper.policy %{buildroot}%{_datadir}/polkit-1/actions/io.unom.punktfunk.dm-helper.policy
|
||||||
|
|
||||||
# vhci-hcd autoload — the usbip transport that makes the virtual Steam Deck controller a
|
# vhci-hcd autoload — the usbip transport that makes the virtual Steam Deck controller a
|
||||||
# real USB device (Steam Input only adopts those; the UHID fallback is invisible to Steam).
|
# real USB device (Steam Input only adopts those; the UHID fallback is invisible to Steam).
|
||||||
install -Dm0644 scripts/punktfunk-modules.conf %{buildroot}%{_prefix}/lib/modules-load.d/punktfunk.conf
|
install -Dm0644 scripts/punktfunk-modules.conf %{buildroot}%{_prefix}/lib/modules-load.d/punktfunk.conf
|
||||||
@@ -383,6 +389,9 @@ install -Dm0644 scripts/punktfunk-scripting.service %{buildroot}%{_userunitdir}/
|
|||||||
%{_bindir}/punktfunk-host
|
%{_bindir}/punktfunk-host
|
||||||
%{_bindir}/punktfunk-tray
|
%{_bindir}/punktfunk-tray
|
||||||
%{_udevrulesdir}/60-punktfunk.rules
|
%{_udevrulesdir}/60-punktfunk.rules
|
||||||
|
%dir %{_libexecdir}/punktfunk
|
||||||
|
%{_libexecdir}/punktfunk/pf-dm-helper
|
||||||
|
%{_datadir}/polkit-1/actions/io.unom.punktfunk.dm-helper.policy
|
||||||
%{_prefix}/lib/modules-load.d/punktfunk.conf
|
%{_prefix}/lib/modules-load.d/punktfunk.conf
|
||||||
%{_prefix}/lib/sysctl.d/99-punktfunk-net.conf
|
%{_prefix}/lib/sysctl.d/99-punktfunk-net.conf
|
||||||
%{_prefix}/lib/firewalld/services/punktfunk-gamestream.xml
|
%{_prefix}/lib/firewalld/services/punktfunk-gamestream.xml
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE policyconfig PUBLIC
|
||||||
|
"-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
|
||||||
|
"http://www.freedesktop.org/standards/PolicyKit/1.0/policyconfig.dtd">
|
||||||
|
<policyconfig>
|
||||||
|
<vendor>Punktfunk</vendor>
|
||||||
|
<vendor_url>https://punktfunk.unom.io</vendor_url>
|
||||||
|
|
||||||
|
<!-- Stop/restore the box's display manager for a managed gamescope takeover. The helper derives
|
||||||
|
the unit from the display-manager.service symlink itself (no caller-chosen units), so this
|
||||||
|
grant is scoped to the box's own local-seat session lifecycle — the same class of operation
|
||||||
|
these distros already authorize for their session switcher (e.g. Nobara's
|
||||||
|
os-session-select, allow_any). allow_any because the host commonly runs sessionless (a
|
||||||
|
lingering user unit, no polkit agent), where interactive auth can never be answered. -->
|
||||||
|
<action id="io.unom.punktfunk.dm-helper">
|
||||||
|
<description>Stop or restore the display manager for a Punktfunk stream</description>
|
||||||
|
<message>Authentication is required to switch the display manager for a Punktfunk stream</message>
|
||||||
|
<defaults>
|
||||||
|
<allow_any>yes</allow_any>
|
||||||
|
<allow_inactive>yes</allow_inactive>
|
||||||
|
<allow_active>yes</allow_active>
|
||||||
|
</defaults>
|
||||||
|
<annotate key="org.freedesktop.policykit.exec.path">/usr/libexec/punktfunk/pf-dm-helper</annotate>
|
||||||
|
</action>
|
||||||
|
</policyconfig>
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Privileged display-manager verbs for the punktfunk managed gamescope takeover.
|
||||||
|
#
|
||||||
|
# On DM-autologin boxes whose display manager does not survive a masked session unit (Nobara's
|
||||||
|
# plasmalogin, unknown DMs), taking the Gaming Mode session over means stopping the display
|
||||||
|
# manager for the length of the stream and restarting it afterwards — root-only operations. The
|
||||||
|
# host invokes this helper via pkexec under its own polkit action
|
||||||
|
# (io.unom.punktfunk.dm-helper.policy, installed by the packages), the same mechanism these
|
||||||
|
# distros use for their own session switching (Nobara's os-session-select).
|
||||||
|
#
|
||||||
|
# The unit is NEVER caller-controlled: it is derived here from the display-manager.service alias
|
||||||
|
# symlink, so the polkit grant's blast radius is exactly "the box's own display manager" — a
|
||||||
|
# local-seat operation, not arbitrary unit management.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
dm_unit() {
|
||||||
|
target=$(readlink /etc/systemd/system/display-manager.service) || {
|
||||||
|
echo "pf-dm-helper: no display-manager.service alias — no display manager to manage" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
basename "$target"
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1-}" in
|
||||||
|
stop)
|
||||||
|
exec systemctl stop "$(dm_unit)"
|
||||||
|
;;
|
||||||
|
restore)
|
||||||
|
dm=$(dm_unit)
|
||||||
|
# reset-failed first: a relogin loop may have tripped the unit's start limit, and a plain
|
||||||
|
# restart would be refused until the accounting is cleared.
|
||||||
|
systemctl reset-failed "$dm" 2>/dev/null || :
|
||||||
|
exec systemctl restart "$dm"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "usage: pf-dm-helper stop|restore" >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@@ -12,6 +12,11 @@ Description=punktfunk management web console
|
|||||||
# web-init generates the login password; the host writes the mgmt token. Order after both.
|
# web-init generates the login password; the host writes the mgmt token. Order after both.
|
||||||
After=punktfunk-web-init.service punktfunk-host.service
|
After=punktfunk-web-init.service punktfunk-host.service
|
||||||
Wants=punktfunk-web-init.service
|
Wants=punktfunk-web-init.service
|
||||||
|
# Retry indefinitely while the host is still writing the mgmt token + identity cert. Without this,
|
||||||
|
# systemd's default rate limit (5 starts / 10 s) plus RestartSec=2 gives up permanently after ~10 s
|
||||||
|
# - so a console enabled before the host's first run stayed dead until someone restarted it by hand
|
||||||
|
# (the same defect the Windows PunktfunkWeb task had).
|
||||||
|
StartLimitIntervalSec=0
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
|
|||||||
@@ -16,17 +16,26 @@ set "CERTFILE=%PFDATA%\cert.pem"
|
|||||||
set "KEYFILE=%PFDATA%\key.pem"
|
set "KEYFILE=%PFDATA%\key.pem"
|
||||||
|
|
||||||
rem The host's `serve` writes the mgmt token + its identity cert/key on first run. Until they exist
|
rem The host's `serve` writes the mgmt token + its identity cert/key on first run. Until they exist
|
||||||
rem we have no credential and no TLS material, so fail and let the task's restart-on-failure retry
|
rem we have no credential and no TLS material, so WAIT rather than silently downgrading to plain HTTP.
|
||||||
rem (mirrors the Linux unit's Restart=on-failure waiting for the host to create them) rather than
|
rem
|
||||||
rem silently downgrading to plain HTTP.
|
rem Wait in-process instead of exiting 1 and hoping the task's restart-on-failure retries: Task
|
||||||
if not exist "%TOKENFILE%" (
|
rem Scheduler does not reliably restart on a plain non-zero exit code, so a console that started
|
||||||
echo [punktfunk-web] mgmt token not present yet at "%TOKENFILE%" - waiting for the host service.
|
rem before the host finished writing those files (the token lands at argument parse, the cert only
|
||||||
exit /b 1
|
rem after the RSA-2048 keygen) used to stay down until the next reboot. ~5 min at 2 s, then give up
|
||||||
)
|
rem so a genuinely broken install still surfaces as a failed task rather than one that runs forever.
|
||||||
if not exist "%CERTFILE%" (
|
rem `timeout` needs a console this task does not have, so `ping -n 3` is the 2-second sleep.
|
||||||
echo [punktfunk-web] host identity cert not present yet at "%CERTFILE%" - waiting for the host service.
|
set /a PFWAITS=0
|
||||||
|
:pfwait
|
||||||
|
if exist "%TOKENFILE%" if exist "%CERTFILE%" goto pfready
|
||||||
|
if %PFWAITS% GEQ 150 (
|
||||||
|
echo [punktfunk-web] gave up waiting for "%TOKENFILE%" + "%CERTFILE%" - is the punktfunk host service running?
|
||||||
exit /b 1
|
exit /b 1
|
||||||
)
|
)
|
||||||
|
if %PFWAITS%==0 echo [punktfunk-web] waiting for the host service to write the mgmt token + identity cert...
|
||||||
|
set /a PFWAITS+=1
|
||||||
|
ping -n 3 127.0.0.1 >nul 2>&1
|
||||||
|
goto pfwait
|
||||||
|
:pfready
|
||||||
|
|
||||||
rem Both files are single KEY=VALUE lines (LF), written 0600/ACL'd: PUNKTFUNK_MGMT_TOKEN=... and
|
rem Both files are single KEY=VALUE lines (LF), written 0600/ACL'd: PUNKTFUNK_MGMT_TOKEN=... and
|
||||||
rem PUNKTFUNK_UI_PASSWORD=... . Split on the first '=' and import each into the environment.
|
rem PUNKTFUNK_UI_PASSWORD=... . Split on the first '=' and import each into the environment.
|
||||||
|
|||||||
+13
-8
@@ -18,16 +18,21 @@ set "CERTFILE=%PFDATA%\cert.pem"
|
|||||||
set "KEYFILE=%PFDATA%\key.pem"
|
set "KEYFILE=%PFDATA%\key.pem"
|
||||||
|
|
||||||
rem The host's `serve` writes the mgmt token + identity cert on first run. Until they exist the proxy
|
rem The host's `serve` writes the mgmt token + identity cert on first run. Until they exist the proxy
|
||||||
rem has no credential and no TLS material, so fail and let restart-on-failure retry (mirrors the
|
rem has no credential and no TLS material, so WAIT for them (mirrors the installed launcher) rather
|
||||||
rem installed launcher / Linux unit) rather than silently serving plain HTTP.
|
rem than silently serving plain HTTP - see scripts\windows\web-run.cmd for why waiting here beats
|
||||||
if not exist "%TOKENFILE%" (
|
rem exiting 1 and relying on the task's restart-on-failure. ~5 min at 2 s, then give up.
|
||||||
echo [punktfunk-web] mgmt token not present yet at "%TOKENFILE%" - waiting for the host service.
|
set /a PFWAITS=0
|
||||||
exit /b 1
|
:pfwait
|
||||||
)
|
if exist "%TOKENFILE%" if exist "%CERTFILE%" goto pfready
|
||||||
if not exist "%CERTFILE%" (
|
if %PFWAITS% GEQ 150 (
|
||||||
echo [punktfunk-web] host identity cert not present yet at "%CERTFILE%" - waiting for the host service.
|
echo [punktfunk-web] gave up waiting for "%TOKENFILE%" + "%CERTFILE%" - is the punktfunk host running?
|
||||||
exit /b 1
|
exit /b 1
|
||||||
)
|
)
|
||||||
|
if %PFWAITS%==0 echo [punktfunk-web] waiting for the host to write the mgmt token + identity cert...
|
||||||
|
set /a PFWAITS+=1
|
||||||
|
ping -n 3 127.0.0.1 >nul 2>&1
|
||||||
|
goto pfwait
|
||||||
|
:pfready
|
||||||
|
|
||||||
rem Both files are single KEY=VALUE lines: PUNKTFUNK_MGMT_TOKEN=... and PUNKTFUNK_UI_PASSWORD=... .
|
rem Both files are single KEY=VALUE lines: PUNKTFUNK_MGMT_TOKEN=... and PUNKTFUNK_UI_PASSWORD=... .
|
||||||
rem Split on the first '=' and import each into the environment.
|
rem Split on the first '=' and import each into the environment.
|
||||||
|
|||||||
Reference in New Issue
Block a user