mirror of
https://github.com/nestriness/nestri.git
synced 2026-09-19 17:25:19 +03:00
The component nescapture, neswire and nescope all talk to, and the only thing in the guest that speaks to the client. It muxes their frames into one iroh QUIC endpoint and fans input back. Renamed from nestri-guest-hub, which named a location rather than a job. Four files came across unchanged -- session.rs, ipc_listener.rs, ticket.rs, screenshot.rs. Between them they mention Steam zero times, and they import only nesprotocol's open modules; the control feature carrying LaunchIntent and SteamIdentity is used exclusively by the three files that are staying closed. The two clusters shared a main.rs and nothing else, so there was no untangling to do -- only a cut. main.rs loses --proton, --steamclient-so, --root and the game uid/gid, and no longer ends by handing the process to a controller. It runs until it is stopped. Deciding when the box is finished belongs to nesinit. The ticket used to leave via that controller, so it needed a new way out: neshub now serves it on a socket and nesinit dials for it. Listening rather than dialling matches every other socket here and means no startup ordering to get wrong. Three tests, where there were none -- the ticket crosses a process boundary as text now, so a round trip that drops a field would otherwise be found by whoever cannot connect.
106 lines
4.1 KiB
Rust
106 lines
4.1 KiB
Rust
//! Asking nescope what is on screen.
|
|
//!
|
|
//! neshub is the listener and nescope dials out, the same way the input socket
|
|
//! works — so this waits for a connection rather than making one, and there is
|
|
//! no race against a compositor that has not started yet.
|
|
//!
|
|
//! Nothing here interprets the pixels. It is transport only -- whatever asked
|
|
//! for the picture decides what it means.
|
|
//!
|
|
//! **Nothing calls this today.** It was neshub's half of a login-QR capture,
|
|
//! and that path is gone -- nessh's own token signs the client in, so no
|
|
//! screenshot is needed to sign anybody in. It is kept because the capture
|
|
//! works and "show me what the guest is displaying" is the first question when
|
|
//! a payload renders black; `nescope-shot` covers the same ground from the
|
|
//! other side. Wire it to a control message or delete it, but do not leave it
|
|
//! half-connected.
|
|
#![allow(dead_code)]
|
|
|
|
use std::path::Path;
|
|
use std::sync::Arc;
|
|
|
|
use anyhow::{Context, Result};
|
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|
use tokio::net::{UnixListener, UnixStream};
|
|
use tokio::sync::Mutex;
|
|
|
|
/// Ask for a picture of what is on screen.
|
|
const REQUEST_CAPTURE: u8 = 0x01;
|
|
|
|
/// Status bytes, as `nescope::screenshot_wire` defines them.
|
|
const STATUS_OK: u8 = 0;
|
|
|
|
/// The connection nescope made, once it has made one.
|
|
///
|
|
/// Held across captures rather than reconnecting: nescope connects once and
|
|
/// serves every request on that connection, so dropping it would mean nothing
|
|
/// could ask again.
|
|
pub type Connection = Arc<Mutex<Option<UnixStream>>>;
|
|
|
|
/// Listen for nescope, and keep the connection for whoever asks later.
|
|
///
|
|
/// Returns as soon as the socket is bound, not when nescope connects — the
|
|
/// caller has other things to start, and a compositor that never dials is a
|
|
/// failure the first capture reports rather than one that blocks startup.
|
|
pub fn listen(path: &Path) -> Result<Connection> {
|
|
// A stale socket file makes bind fail with EADDRINUSE, which reads as
|
|
// "something is already listening" when nothing is.
|
|
let _ = std::fs::remove_file(path);
|
|
let listener = UnixListener::bind(path)
|
|
.with_context(|| format!("could not listen on {}", path.display()))?;
|
|
tracing::info!("listening for nescope screenshots on {}", path.display());
|
|
|
|
let connection: Connection = Arc::new(Mutex::new(None));
|
|
let slot = Arc::clone(&connection);
|
|
tokio::spawn(async move {
|
|
loop {
|
|
match listener.accept().await {
|
|
Ok((stream, _)) => {
|
|
tracing::info!("nescope connected for screenshots");
|
|
*slot.lock().await = Some(stream);
|
|
}
|
|
Err(e) => {
|
|
tracing::warn!("accepting a screenshot connection failed: {e}");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
Ok(connection)
|
|
}
|
|
|
|
/// One capture. `None` means nescope had nothing to show, which is not an
|
|
/// error — a client that has not drawn yet is the normal case while waiting.
|
|
pub async fn capture_on(connection: &Connection) -> Result<Option<(u32, u32, Vec<u8>)>> {
|
|
let mut guard = connection.lock().await;
|
|
let Some(stream) = guard.as_mut() else {
|
|
anyhow::bail!("nescope has not connected for screenshots yet");
|
|
};
|
|
|
|
stream
|
|
.write_all(&[REQUEST_CAPTURE])
|
|
.await
|
|
.context("could not ask nescope for a capture")?;
|
|
|
|
let mut header = [0u8; 9];
|
|
stream
|
|
.read_exact(&mut header)
|
|
.await
|
|
.context("nescope did not answer a capture request")?;
|
|
|
|
if header[0] != STATUS_OK {
|
|
// Every non-Ok status means "no pixels", and the distinctions between
|
|
// them are a debugging matter for `nescope-shot` rather than anything
|
|
// this can act on differently.
|
|
return Ok(None);
|
|
}
|
|
let width = u32::from_le_bytes(header[1..5].try_into().unwrap());
|
|
let height = u32::from_le_bytes(header[5..9].try_into().unwrap());
|
|
let mut rgba = vec![0u8; (width as usize) * (height as usize) * 4];
|
|
stream
|
|
.read_exact(&mut rgba)
|
|
.await
|
|
.context("a capture was cut short")?;
|
|
Ok(Some((width, height, rgba)))
|
|
}
|