mirror of
https://github.com/sol1/rustguac.git
synced 2026-09-10 01:26:06 +00:00
feat(multimon): A1 receive plumbing for multi-monitor (SPICE)
Client library (Client.js): new onmultimonlayout callback + a multimon-layout layerPropertyHandler (mirrors multi-touch), and sendSize extended to carry the optional per-monitor x_position/top_offset. rustguac: SpiceParams.secondary_monitors + a secondary-monitors connect arg so guacd advertises the allowed monitor count; CreateSessionRequest.max_monitors (secondary = max-1) wired through both SPICE branches. client.html: onargv reads the secondary-monitors count and onmultimonlayout parses the layout JSON (logging for now; per-monitor windows are A2). Verified on the canary vs VMID 300: client logs 'server allows 2 monitors' and receives a multimon-layout. The layout is request-driven (guacd activates a second guest head only when the client sends a size for monitor 1), so the second monitor appears in A2.
This commit is contained in:
@@ -2513,6 +2513,9 @@ pub async fn ab_connect_entry(
|
||||
proxmox_token_id: ab_entry.proxmox_token_id,
|
||||
proxmox_token_secret: ab_entry.proxmox_token_secret,
|
||||
proxmox_verify_tls: ab_entry.proxmox_verify_tls,
|
||||
// Entry-stored monitor count comes with the connections UI in a later
|
||||
// phase; ad-hoc/API sessions set max_monitors directly on the request.
|
||||
max_monitors: None,
|
||||
};
|
||||
|
||||
let proxies = trusted.map(|Extension(t)| t.0).unwrap_or_default();
|
||||
@@ -4185,6 +4188,7 @@ pub async fn quick_connect(
|
||||
proxmox_token_id: None,
|
||||
proxmox_token_secret: None,
|
||||
proxmox_verify_tls: None,
|
||||
max_monitors: None,
|
||||
};
|
||||
|
||||
tracing::info!(
|
||||
@@ -4302,6 +4306,7 @@ pub async fn quick_connect(
|
||||
proxmox_token_id: None,
|
||||
proxmox_token_secret: None,
|
||||
proxmox_verify_tls: None,
|
||||
max_monitors: None,
|
||||
};
|
||||
|
||||
match manager.create_session(create_req, admin_name).await {
|
||||
|
||||
@@ -103,6 +103,10 @@ pub struct SpiceParams {
|
||||
pub disable_copy: bool,
|
||||
pub disable_paste: bool,
|
||||
pub enable_audio: bool,
|
||||
/// Number of secondary monitors to allow (beyond the primary). guacd
|
||||
/// advertises this to the client as `secondary-monitors` so a multi-monitor
|
||||
/// client can offer the right number of monitor windows. 0 = single monitor.
|
||||
pub secondary_monitors: u32,
|
||||
}
|
||||
|
||||
/// RDP connection parameters to pass to guacd.
|
||||
@@ -389,6 +393,7 @@ pub async fn connect_and_handshake(
|
||||
"enable-audio" => if p.enable_audio { "true" } else { "false" }.into(),
|
||||
"disable-copy" => if p.disable_copy { "true" } else { "false" }.into(),
|
||||
"disable-paste" => if p.disable_paste { "true" } else { "false" }.into(),
|
||||
"secondary-monitors" => p.secondary_monitors.to_string(),
|
||||
_ => {
|
||||
tracing::debug!("Unknown guacd SPICE parameter '{}', sending empty", name);
|
||||
String::new()
|
||||
|
||||
@@ -176,6 +176,10 @@ pub struct CreateSessionRequest {
|
||||
/// Verify the PVE API server's TLS certificate (default false; PVE ships a
|
||||
/// self-signed cluster cert). Also controls SPICE-proxy cert verification.
|
||||
pub proxmox_verify_tls: Option<bool>,
|
||||
/// Total number of monitors to offer (SPICE/Proxmox multi-monitor). guacd
|
||||
/// is told `secondary-monitors = max_monitors - 1`, which it advertises to
|
||||
/// the client. Default 1 (single monitor).
|
||||
pub max_monitors: Option<u32>,
|
||||
}
|
||||
|
||||
/// Session status in the lifecycle.
|
||||
@@ -984,6 +988,8 @@ impl SessionManager {
|
||||
disable_copy: req.disable_copy.unwrap_or(false),
|
||||
disable_paste: req.disable_paste.unwrap_or(false),
|
||||
enable_audio: false,
|
||||
// Secondary monitors = total requested minus the primary.
|
||||
secondary_monitors: req.max_monitors.unwrap_or(1).saturating_sub(1),
|
||||
};
|
||||
tracing::info!(
|
||||
session_id = %session_id,
|
||||
@@ -1132,6 +1138,8 @@ impl SessionManager {
|
||||
disable_copy: req.disable_copy.unwrap_or(false),
|
||||
disable_paste: req.disable_paste.unwrap_or(false),
|
||||
enable_audio: false,
|
||||
// Secondary monitors = total requested minus the primary.
|
||||
secondary_monitors: req.max_monitors.unwrap_or(1).saturating_sub(1),
|
||||
};
|
||||
|
||||
// `cfg.host` is an opaque PVE routing token, used as the display
|
||||
|
||||
@@ -601,6 +601,33 @@
|
||||
toggleTab.addEventListener('click', function() { toggleClipboardPanel(); });
|
||||
document.body.appendChild(toggleTab);
|
||||
|
||||
// ── Multi-monitor (A1: receive + log; per-monitor windows are A2) ──
|
||||
// Max monitors the server allows, delivered via a "secondary-monitors"
|
||||
// argv stream. secondary-monitors = N means N+1 monitors total.
|
||||
var maxMonitors = 1;
|
||||
client.onargv = function(stream, mimetype, name) {
|
||||
var reader = new Guacamole.StringReader(stream);
|
||||
var buf = '';
|
||||
reader.ontext = function(t) { buf += t; };
|
||||
reader.onend = function() {
|
||||
if (name !== 'secondary-monitors') return;
|
||||
var secondary = parseInt(buf, 10);
|
||||
if (!isNaN(secondary)) maxMonitors = secondary + 1;
|
||||
console.log('[rustguac] multimon: server allows ' + maxMonitors
|
||||
+ ' monitor(s) (secondary-monitors=' + buf.trim() + ')');
|
||||
};
|
||||
};
|
||||
// Current monitor layout (each monitor's rect within the combined
|
||||
// framebuffer), JSON via a "multimon-layout" set param.
|
||||
client.onmultimonlayout = function(layer, value) {
|
||||
try {
|
||||
var layout = JSON.parse(value);
|
||||
console.log('[rustguac] multimon-layout:', layout);
|
||||
} catch (e) {
|
||||
console.warn('[rustguac] multimon-layout parse error:', e, value);
|
||||
}
|
||||
};
|
||||
|
||||
// ── Receive clipboard from remote ──
|
||||
client.onclipboard = function(stream, mimetype) {
|
||||
if (mimetype !== 'text/plain') {
|
||||
|
||||
+35
-3
@@ -343,13 +343,20 @@ Guacamole.Client = function(tunnel) {
|
||||
* @param {!number} height
|
||||
* The height of the screen.
|
||||
*/
|
||||
this.sendSize = function(width, height) {
|
||||
this.sendSize = function(width, height, x_position, top_offset) {
|
||||
|
||||
// Do not send requests if not connected
|
||||
if (!isConnected())
|
||||
return;
|
||||
|
||||
tunnel.sendMessage("size", width, height);
|
||||
// Include the optional per-monitor position (x_position, top_offset)
|
||||
// when provided, so multi-monitor clients can declare where each
|
||||
// monitor sits within the combined framebuffer. Single-monitor
|
||||
// callers omit them and the server treats the monitor as primary.
|
||||
if (x_position !== undefined && top_offset !== undefined)
|
||||
tunnel.sendMessage("size", width, height, x_position, top_offset);
|
||||
else
|
||||
tunnel.sendMessage("size", width, height);
|
||||
|
||||
};
|
||||
|
||||
@@ -860,9 +867,26 @@ Guacamole.Client = function(tunnel) {
|
||||
*/
|
||||
this.onargv = null;
|
||||
|
||||
/**
|
||||
* Fired when the server sends an updated multi-monitor layout via the
|
||||
* "multimon-layout" parameter on a visible layer. The value is a JSON
|
||||
* string mapping monitor index to its rectangle within the combined
|
||||
* framebuffer, e.g. {"0":{"left":0,"top":0,"width":1920,"height":1080}}.
|
||||
* Used by multi-monitor clients to split the framebuffer into per-monitor
|
||||
* windows.
|
||||
*
|
||||
* @event
|
||||
* @param {!Guacamole.Display.VisibleLayer} layer
|
||||
* The layer the layout applies to (typically the default layer).
|
||||
*
|
||||
* @param {!string} value
|
||||
* The multimon-layout JSON string.
|
||||
*/
|
||||
this.onmultimonlayout = null;
|
||||
|
||||
/**
|
||||
* Fired when the clipboard of the remote client is changing.
|
||||
*
|
||||
*
|
||||
* @event
|
||||
* @param {!Guacamole.InputStream} stream
|
||||
* The stream that will receive clipboard data from the server.
|
||||
@@ -1045,6 +1069,14 @@ Guacamole.Client = function(tunnel) {
|
||||
if (guac_client.onmultitouch && layer instanceof Guacamole.Display.VisibleLayer)
|
||||
guac_client.onmultitouch(layer, parseInt(value));
|
||||
|
||||
},
|
||||
|
||||
"multimon-layout" : function layerMultimonLayout(layer, value) {
|
||||
|
||||
// Expose the multi-monitor layout only for true visible layers
|
||||
if (guac_client.onmultimonlayout && layer instanceof Guacamole.Display.VisibleLayer)
|
||||
guac_client.onmultimonlayout(layer, value);
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user