Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cdacd5636e | ||
|
|
47f01149bf | ||
|
|
20568d988f |
@@ -66,6 +66,16 @@ use zkde::zkde_screencast_unstable_v1::ZkdeScreencastUnstableV1 as Screencast;
|
||||
const POINTER_METADATA: u32 = 4;
|
||||
const POINTER_EMBEDDED: u32 = 2;
|
||||
|
||||
/// Marks the one KWin refusal a retry can clear: the disabled-output repair ran and changed the
|
||||
/// box between attempts ([`kwin_output_mgmt::enable_disabled_output`]).
|
||||
///
|
||||
/// It is load-bearing in TWO places and both are easy to break. The opener keys on it to skip the
|
||||
/// `KWin virtual output failed` wrapper below — and that wrapper's prefix is exactly what the
|
||||
/// host's `is_permanent_build_error` matches to short-circuit the retry loop, so a repaired
|
||||
/// refusal carrying it would be classified permanent and the retry that consumes the repair would
|
||||
/// never run. It is also the human-readable half of the message; keep it a phrase, not a code.
|
||||
const REPAIRED_HINT: &str = "enabled it over output management";
|
||||
|
||||
/// The name we give the created output; KWin exposes it to output-management as `Virtual-<name>`.
|
||||
const VOUT_NAME: &str = "punktfunk";
|
||||
|
||||
@@ -268,6 +278,10 @@ impl VirtualDisplay for KwinDisplay {
|
||||
.context("spawn KWin virtual-output thread")?;
|
||||
match setup_rx.recv_timeout(OPENER_BUDGET) {
|
||||
Ok(Ok(v)) => Ok((v, stop)),
|
||||
// Repaired: report it as-is. The wrapper below would prepend the phrase the host
|
||||
// reads as "permanent, do not retry", and this is the one refusal whose retry is
|
||||
// the entire point — the repair only fixes the NEXT request.
|
||||
Ok(Err(e)) if e.contains(REPAIRED_HINT) => bail!("{e}"),
|
||||
// KWin's reason is TRANSLATED into the session's language, so it is often
|
||||
// unsearchable for the person reading the log. Say what it means once, here.
|
||||
Ok(Err(e)) => bail!(
|
||||
@@ -1793,14 +1807,41 @@ fn run(
|
||||
);
|
||||
|
||||
// Pump events until KWin reports the node id (or an error, or the budget).
|
||||
let node_id = await_created(
|
||||
//
|
||||
// A refusal here is where the KWin >= 6.6 disabled-output trap lands, and it is repairable
|
||||
// FROM INSIDE THIS SCOPE and nowhere else: KWin destroys the output when our stream is
|
||||
// destroyed, so the connection has to stay up while we enable it (see
|
||||
// [`kwin_output_mgmt::enable_disabled_output`] for why the output is still alive at all, and
|
||||
// why enabling it fixes the NEXT request rather than this one).
|
||||
let node_id = match await_created(
|
||||
&conn,
|
||||
&mut queue,
|
||||
&mut state,
|
||||
stop,
|
||||
"stream_virtual_output",
|
||||
started,
|
||||
)?;
|
||||
) {
|
||||
Ok(id) => id,
|
||||
Err(e) => {
|
||||
// `Virtual-<name>` is the address KWin exposes our output under (the same prefix the
|
||||
// topology path resolves against).
|
||||
match crate::kwin_output_mgmt::enable_disabled_output(&format!("Virtual-{name}")) {
|
||||
// Deliberately does NOT carry the "KWin virtual output failed" prefix: that string
|
||||
// is what marks a KWin refusal PERMANENT for the session's retry loop, and this is
|
||||
// the one refusal where something DID change between attempts. Retrying is the
|
||||
// whole point of repairing.
|
||||
Some(repaired) => bail!(
|
||||
"KWin created the virtual output disabled and refused to stream it ({e}); \
|
||||
{REPAIRED_HINT} (head {repaired}) — the retry picks up the configuration \
|
||||
KWin just persisted"
|
||||
),
|
||||
// Nothing to repair (no such head, already enabled, or the apply was refused):
|
||||
// the refusal stands, and its own prefix keeps it permanent so the session fails
|
||||
// fast instead of burning the retry budget on an unchanged box.
|
||||
None => return Err(e),
|
||||
}
|
||||
}
|
||||
};
|
||||
setup_tx
|
||||
.send(Ok(node_id))
|
||||
.map_err(|_| anyhow!("virtual-output opener went away"))?;
|
||||
|
||||
@@ -1274,6 +1274,76 @@ pub(crate) fn reenable_outputs(outputs: &[(String, String)]) -> bool {
|
||||
complete
|
||||
}
|
||||
|
||||
/// Enable a virtual output KWin created but left DISABLED, addressed by the `Virtual-<name>`
|
||||
/// prefix it exposes ours under. Returns the head's name when one matched, was disabled, and the
|
||||
/// enable applied.
|
||||
///
|
||||
/// This is the repair for the KWin ≥ 6.6 refusal (`"Could not find output"`, translated into the
|
||||
/// session's language). `streamVirtualOutput` there creates the output on the backend and then
|
||||
/// hands `workspace()->findOutput(output)` to the stream — and that returns null for an output the
|
||||
/// workspace does not manage, which `wantsToManage` defines as `isEnabled() && !isNonDesktop()`.
|
||||
/// KWin 6.4/6.5 passed the backend output straight through, so a disabled one streamed anyway;
|
||||
/// from 6.6 it is a hard refusal, and one that repeats forever: the host asks for a STABLE
|
||||
/// per-client name so KWin persists that client's scale and mode, and a stored setup naming it
|
||||
/// `enabled: false` is therefore reapplied to every future session.
|
||||
///
|
||||
/// Two properties of KWin make the repair possible, both verified against Plasma/6.7:
|
||||
///
|
||||
/// * `sendFailed` only sends the event — it does not emit `finished`, and `removeVirtualOutput` is
|
||||
/// wired to `finished`. So the disabled output stays alive for exactly as long as the caller
|
||||
/// holds its (failed) stream open, which is the window this runs in.
|
||||
/// * `WaylandServer::handleOutputAdded` offers EVERY backend output to the output-device registry,
|
||||
/// gating only placeholders and non-desktop ones. A disabled output has no `wl_output` — that
|
||||
/// side is gated on the workspace — but it is addressable over `kde_output_management_v2`.
|
||||
///
|
||||
/// Enabling it through output management is a user-applied configuration, so KWin persists it
|
||||
/// against that output's identity: the caller's next `stream_virtual_output` under the same name
|
||||
/// finds a stored setup that enables it. Which is why the caller must RETRY after this returns
|
||||
/// `Some` — the request that failed cannot be salvaged, only the one after it.
|
||||
pub(crate) fn enable_disabled_output(prefix: &str) -> Option<String> {
|
||||
let mut sess = Session::open("enable_disabled").ok()?;
|
||||
let deadline = Instant::now() + OP_BUDGET;
|
||||
// Newest-wins, exactly as the supersede resolve elsewhere in this file: a reconnect can leave
|
||||
// a predecessor of the same name briefly announced, and enabling THAT one repairs an output
|
||||
// that is already going away.
|
||||
let dev = sess
|
||||
.state
|
||||
.devices
|
||||
.values()
|
||||
.filter(|d| d.name.as_deref().is_some_and(|n| n.starts_with(prefix)) && d.proxy.is_some())
|
||||
.max_by_key(|d| (d.global, d.seq))
|
||||
.cloned()?;
|
||||
let name = dev.name.clone()?;
|
||||
if dev.enabled {
|
||||
// Not the shape we repair. Say so rather than applying a no-op config that would `applied`
|
||||
// successfully and read as a fix — the caller decides whether to retry on this.
|
||||
tracing::debug!(
|
||||
%name,
|
||||
"KWin output management: our virtual output is already enabled — nothing to repair"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
let proxy = dev.proxy.as_ref()?;
|
||||
let config = sess.new_config();
|
||||
config.enable(proxy, 1);
|
||||
let ok = sess.apply(&config, deadline);
|
||||
config.destroy();
|
||||
if !ok {
|
||||
tracing::warn!(
|
||||
%name,
|
||||
reason = ?sess.state.failure_reason,
|
||||
"KWin output management: could not enable the virtual output KWin created disabled"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
tracing::info!(
|
||||
%name,
|
||||
"KWin output management: KWin created our virtual output DISABLED and refused to stream \
|
||||
it; enabled it — KWin persists that, so the retry's request comes back enabled"
|
||||
);
|
||||
Some(name)
|
||||
}
|
||||
|
||||
/// Position the output identified by `uuid` at `(x, y)` in the desktop layout, in-process. Returns
|
||||
/// `true` if applied; `false` tells the caller to fall back to `kscreen-doctor`.
|
||||
pub(crate) fn set_position(uuid: &str, x: i32, y: i32) -> bool {
|
||||
|
||||
@@ -5345,6 +5345,17 @@ mod tests {
|
||||
"spawn gamescope (is it installed? `apt install gamescope`)"
|
||||
));
|
||||
assert!(is_permanent_build_error("virtual displays require Linux"));
|
||||
// The ONE KWin refusal that must stay retryable: pf-vdisplay repaired the box (it enabled
|
||||
// the output KWin created disabled, which KWin persists), so the next attempt is not the
|
||||
// same attempt. That path deliberately reports WITHOUT the `KWin virtual output failed`
|
||||
// prefix above — if it ever regains it, the retry that consumes the repair stops running
|
||||
// and the repair is dead code.
|
||||
assert!(!is_permanent_build_error(
|
||||
"create virtual output: KWin created the virtual output disabled and refused to \
|
||||
stream it (stream_virtual_output failed: Não foi possível encontrar saída); enabled \
|
||||
it over output management (head Virtual-punktfunk-a1b2) — the retry picks up the \
|
||||
configuration KWin just persisted"
|
||||
));
|
||||
// Transient: negotiation/timeout races — exactly what backoff is for.
|
||||
assert!(!is_permanent_build_error(
|
||||
"first frame: no PipeWire frame within 10s (node 42): format negotiation never completed"
|
||||
|
||||
Reference in New Issue
Block a user