@desklink/host 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -9
- package/bin/desklink-host.mjs +2 -1
- package/dist/bridge.d.ts +3 -4
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js +18 -7
- package/dist/bridge.js.map +1 -1
- package/dist/engineProcess.d.ts.map +1 -1
- package/dist/engineProcess.js +0 -1
- package/dist/engineProcess.js.map +1 -1
- package/dist/protocol.d.ts +4 -3
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js.map +1 -1
- package/docs/BOUNDARY.md +8 -8
- package/docs/PROTOCOL.md +72 -45
- package/engine/Cargo.lock +1 -1
- package/engine/Cargo.toml +1 -1
- package/engine/build.rs +1 -0
- package/engine/native/agent_indicator.h +57 -0
- package/engine/native/agent_overlay_wayland.c +47 -26
- package/engine/native/agent_overlay_x11.c +116 -57
- package/engine/src/capture.rs +4 -2
- package/engine/src/clipboard.rs +174 -0
- package/engine/src/indicator.rs +175 -10
- package/engine/src/main.rs +2 -2
- package/engine/src/peer.rs +10 -4
- package/engine/src/protocol.rs +7 -10
- package/engine/src/session.rs +305 -209
- package/engine/src/x11.rs +29 -29
- package/engine/src/x11_mac.rs +4 -0
- package/examples/reference-client.html +3 -5
- package/package.json +3 -3
package/engine/src/session.rs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
//! One authorized desktop session: capture in, VP9 out, control channel in,
|
|
2
2
|
//! desktop input out, and a bounded teardown.
|
|
3
3
|
//!
|
|
4
|
-
//! The
|
|
5
|
-
//!
|
|
6
|
-
//!
|
|
4
|
+
//! The consumer's pairing is the authority for all available access. The engine
|
|
5
|
+
//! has no second identity system, no account and no notion of which application
|
|
6
|
+
//! asked.
|
|
7
7
|
|
|
8
8
|
use crate::capture::{self, Capture};
|
|
9
9
|
use crate::clipboard;
|
|
@@ -15,9 +15,7 @@ use crate::peer::{
|
|
|
15
15
|
h264_profile_level_id, PathReport, PeerEvent, TransportOptions, VideoCodec, VideoPeer,
|
|
16
16
|
};
|
|
17
17
|
use crate::portal::{self, SelectedSource};
|
|
18
|
-
use crate::protocol::{
|
|
19
|
-
ControlMessage, ControlReply, EncodedCodec, Permission, PointerPhase, SourceRequest,
|
|
20
|
-
};
|
|
18
|
+
use crate::protocol::{ControlMessage, ControlReply, EncodedCodec, PointerPhase, SourceRequest};
|
|
21
19
|
use crate::x11::X11Desktop;
|
|
22
20
|
use anyhow::{Context, Result};
|
|
23
21
|
use std::sync::atomic::{AtomicBool, Ordering};
|
|
@@ -192,6 +190,98 @@ impl Drop for X11Capture {
|
|
|
192
190
|
}
|
|
193
191
|
}
|
|
194
192
|
|
|
193
|
+
#[cfg(target_os = "linux")]
|
|
194
|
+
unsafe extern "C" {
|
|
195
|
+
fn desklink_indicator_covered(
|
|
196
|
+
x: f64,
|
|
197
|
+
y: f64,
|
|
198
|
+
opacity: f64,
|
|
199
|
+
click: f64,
|
|
200
|
+
typed: f64,
|
|
201
|
+
threshold: u32,
|
|
202
|
+
reveal: f64,
|
|
203
|
+
) -> i32;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/// The X11 agent cue is a real window, so root grabs contain it. Restore
|
|
207
|
+
/// exactly the pixels the announced cues cover (one pixel wider for edge
|
|
208
|
+
/// rounding) from the last clean frame; everything else, including what lies
|
|
209
|
+
/// inside the halo, stays live.
|
|
210
|
+
#[cfg(target_os = "linux")]
|
|
211
|
+
struct CueMask {
|
|
212
|
+
cues: crate::indicator::Cues,
|
|
213
|
+
clean: Option<Vec<u8>>,
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
#[cfg(target_os = "linux")]
|
|
217
|
+
impl CueMask {
|
|
218
|
+
fn new(cues: crate::indicator::Cues) -> Self {
|
|
219
|
+
Self { cues, clean: None }
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
fn snapshot(&self) -> Vec<crate::indicator::Cue> {
|
|
223
|
+
self.cues
|
|
224
|
+
.lock()
|
|
225
|
+
.map(|mut feed| feed.drain())
|
|
226
|
+
.unwrap_or_default()
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/// `before` is the snapshot taken when the grab started: with the states
|
|
230
|
+
/// announced by the time it returned, it covers whatever the grab saw.
|
|
231
|
+
fn apply(
|
|
232
|
+
&mut self,
|
|
233
|
+
raw: &mut [u8],
|
|
234
|
+
width: usize,
|
|
235
|
+
height: usize,
|
|
236
|
+
mut cues: Vec<crate::indicator::Cue>,
|
|
237
|
+
) {
|
|
238
|
+
cues.extend(self.snapshot());
|
|
239
|
+
if let Some(clean) = self.clean.as_ref().filter(|clean| clean.len() == raw.len()) {
|
|
240
|
+
for cue in cues {
|
|
241
|
+
let side = cue.size as usize + 2;
|
|
242
|
+
let mut covered = vec![false; side * side];
|
|
243
|
+
for row in 0..cue.size as usize {
|
|
244
|
+
for col in 0..cue.size as usize {
|
|
245
|
+
let half = cue.size as f64 / 2.0;
|
|
246
|
+
covered[(row + 1) * side + col + 1] = unsafe {
|
|
247
|
+
desklink_indicator_covered(
|
|
248
|
+
(col as f64 - half) / cue.unit,
|
|
249
|
+
(row as f64 - half) / cue.unit,
|
|
250
|
+
cue.opacity,
|
|
251
|
+
cue.click,
|
|
252
|
+
cue.typed,
|
|
253
|
+
cue.threshold,
|
|
254
|
+
cue.reveal,
|
|
255
|
+
)
|
|
256
|
+
} != 0;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
for row in 0..side {
|
|
260
|
+
for col in 0..side {
|
|
261
|
+
let near =
|
|
262
|
+
|r: usize, c: usize| r < side && c < side && covered[r * side + c];
|
|
263
|
+
if !(near(row, col)
|
|
264
|
+
|| near(row.wrapping_sub(1), col)
|
|
265
|
+
|| near(row + 1, col)
|
|
266
|
+
|| near(row, col.wrapping_sub(1))
|
|
267
|
+
|| near(row, col + 1))
|
|
268
|
+
{
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
let (x, y) = (cue.x - 1 + col as i64, cue.y - 1 + row as i64);
|
|
272
|
+
if x < 0 || y < 0 || x >= width as i64 || y >= height as i64 {
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
let at = (y as usize * width + x as usize) * 4;
|
|
276
|
+
raw[at..at + 4].copy_from_slice(&clean[at..at + 4]);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
self.clean = Some(raw.to_vec());
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
195
285
|
/// What a chosen capture backend gives the session.
|
|
196
286
|
struct Selected {
|
|
197
287
|
source: SelectedSource,
|
|
@@ -210,6 +300,7 @@ fn select_x11(
|
|
|
210
300
|
max_fps: u32,
|
|
211
301
|
metrics: Arc<Mutex<Metrics>>,
|
|
212
302
|
#[cfg(target_os = "linux")] cursor_sink: Option<crate::cursor::CursorSink>,
|
|
303
|
+
#[cfg(target_os = "linux")] cues: Option<crate::indicator::Cues>,
|
|
213
304
|
sink: capture::FrameSink,
|
|
214
305
|
on_stop: Box<dyn Fn(String) + Send>,
|
|
215
306
|
) -> Result<Selected> {
|
|
@@ -250,6 +341,8 @@ fn select_x11(
|
|
|
250
341
|
let mut first = true;
|
|
251
342
|
#[cfg(target_os = "linux")]
|
|
252
343
|
let mut cursor = cursor_sink.map(crate::cursor::Reporter::new);
|
|
344
|
+
#[cfg(target_os = "linux")]
|
|
345
|
+
let mut mask = cues.map(CueMask::new);
|
|
253
346
|
while !stop.load(Ordering::SeqCst) {
|
|
254
347
|
#[cfg(target_os = "linux")]
|
|
255
348
|
if let Some(cursor) = cursor.as_mut() {
|
|
@@ -272,10 +365,19 @@ fn select_x11(
|
|
|
272
365
|
}
|
|
273
366
|
let started = Instant::now();
|
|
274
367
|
let grabbed = Instant::now();
|
|
368
|
+
#[cfg(target_os = "linux")]
|
|
275
369
|
let frame = {
|
|
370
|
+
let before = mask.as_ref().map(CueMask::snapshot);
|
|
276
371
|
let mut desktop = lock(&desktop);
|
|
277
|
-
desktop.
|
|
372
|
+
desktop.capture_raw().and_then(|mut raw| {
|
|
373
|
+
if let (Some(mask), Some(before)) = (mask.as_mut(), before) {
|
|
374
|
+
mask.apply(&mut raw, width as usize, height as usize, before);
|
|
375
|
+
}
|
|
376
|
+
Ok((desktop.convert(&raw, max_width, max_height)?, raw))
|
|
377
|
+
})
|
|
278
378
|
};
|
|
379
|
+
#[cfg(not(target_os = "linux"))]
|
|
380
|
+
let frame = lock(&desktop).capture_with_pixels(max_width, max_height);
|
|
279
381
|
let grab_micros = grabbed.elapsed().as_micros() as u64;
|
|
280
382
|
match frame {
|
|
281
383
|
Ok((frame, raw)) => {
|
|
@@ -593,7 +695,6 @@ pub struct OpenRequest {
|
|
|
593
695
|
pub cursor: crate::protocol::CursorMode,
|
|
594
696
|
/** Which desktop to capture; absent means the portal. */
|
|
595
697
|
pub source: Option<SourceRequest>,
|
|
596
|
-
pub permissions: Vec<Permission>,
|
|
597
698
|
pub max_width: usize,
|
|
598
699
|
pub max_height: usize,
|
|
599
700
|
pub bitrate_kbps: u32,
|
|
@@ -684,7 +785,6 @@ enum PointDesktop {
|
|
|
684
785
|
struct Inner {
|
|
685
786
|
id: String,
|
|
686
787
|
generation: u64,
|
|
687
|
-
permissions: Vec<Permission>,
|
|
688
788
|
source: SelectedSource,
|
|
689
789
|
geometry: serde_json::Value,
|
|
690
790
|
metrics: Arc<Mutex<Metrics>>,
|
|
@@ -765,12 +865,10 @@ pub fn capabilities() -> serde_json::Value {
|
|
|
765
865
|
} else {
|
|
766
866
|
"none"
|
|
767
867
|
};
|
|
768
|
-
// Clipboard transfer needs Wayland; writes also need a selection server
|
|
769
|
-
// that can outlive this engine. Do not offer it when that tool is absent.
|
|
770
|
-
let clipboard = wayland_clipboard_available();
|
|
771
868
|
let x11 = crate::x11::X11Desktop::connect(None)
|
|
772
869
|
.map(|desktop| serde_json::json!([desktop.screen_size().0, desktop.screen_size().1]))
|
|
773
870
|
.unwrap_or(serde_json::Value::Null);
|
|
871
|
+
let clipboard = wayland_clipboard_available();
|
|
774
872
|
let (input, grant_state, unavailable) = match crate::input::probe() {
|
|
775
873
|
Ok(()) => (serde_json::json!(true), "granted", serde_json::Value::Null),
|
|
776
874
|
Err(unavailable) => (
|
|
@@ -909,19 +1007,6 @@ impl Session {
|
|
|
909
1007
|
request: OpenRequest,
|
|
910
1008
|
events: tokio_mpsc::UnboundedSender<Notice>,
|
|
911
1009
|
) -> std::result::Result<Self, SessionError> {
|
|
912
|
-
if !request.permissions.contains(&Permission::View) {
|
|
913
|
-
return Err(SessionError::new(
|
|
914
|
-
"permission",
|
|
915
|
-
"session.open requires the view permission",
|
|
916
|
-
));
|
|
917
|
-
}
|
|
918
|
-
#[cfg(target_os = "windows")]
|
|
919
|
-
if request.agent_indicator {
|
|
920
|
-
return Err(SessionError::new(
|
|
921
|
-
"indicator-unavailable",
|
|
922
|
-
"Windows agent indicator is not implemented in this build.",
|
|
923
|
-
));
|
|
924
|
-
}
|
|
925
1010
|
if request.cursor == crate::protocol::CursorMode::Hidden {
|
|
926
1011
|
#[cfg(not(target_os = "linux"))]
|
|
927
1012
|
return Err(SessionError::new(
|
|
@@ -936,7 +1021,6 @@ impl Session {
|
|
|
936
1021
|
}
|
|
937
1022
|
}
|
|
938
1023
|
let id = opaque_id();
|
|
939
|
-
let wants_control = request.permissions.contains(&Permission::Control);
|
|
940
1024
|
let wants_x11 = matches!(request.source, Some(SourceRequest::X11 { .. }));
|
|
941
1025
|
let max_fps = request.max_fps.clamp(1, 60);
|
|
942
1026
|
// A source the consumer feeds needs no capture backend, no encoder and
|
|
@@ -953,23 +1037,26 @@ impl Session {
|
|
|
953
1037
|
// Which desktop decides which input path is even available: an X display
|
|
954
1038
|
// takes XTest, which cannot reach any other session, while a portal
|
|
955
1039
|
// desktop needs kernel input access.
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
if crate::input::probe().is_err() {
|
|
960
|
-
#[cfg(target_os = "macos")]
|
|
961
|
-
let _ = crate::input::request_access();
|
|
962
|
-
if let Err(unavailable) = crate::input::probe() {
|
|
963
|
-
return Err(SessionError::new(
|
|
964
|
-
"input-unavailable",
|
|
965
|
-
format!("{}; {}", unavailable.reason, unavailable.remedy),
|
|
966
|
-
));
|
|
967
|
-
}
|
|
968
|
-
}
|
|
1040
|
+
#[cfg(target_os = "macos")]
|
|
1041
|
+
if crate::input::probe().is_err() {
|
|
1042
|
+
let _ = crate::input::request_access();
|
|
969
1043
|
}
|
|
970
1044
|
|
|
971
1045
|
let metrics = Arc::new(Mutex::new(Metrics::default()));
|
|
972
1046
|
let mut indicator = None;
|
|
1047
|
+
// The X11 cue starts before capture: its window is in root pixels, so
|
|
1048
|
+
// the capture thread needs the announced cue states from the first grab.
|
|
1049
|
+
#[cfg(target_os = "linux")]
|
|
1050
|
+
if request.agent_indicator && wants_x11 {
|
|
1051
|
+
let display = match &request.source {
|
|
1052
|
+
Some(SourceRequest::X11 { display }) => display.clone(),
|
|
1053
|
+
_ => None,
|
|
1054
|
+
}
|
|
1055
|
+
.or_else(|| std::env::var("DISPLAY").ok());
|
|
1056
|
+
if let Some(display) = display {
|
|
1057
|
+
indicator = crate::indicator::Indicator::start(&display, 0, 0).ok();
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
973
1060
|
let (frame_tx, frame_rx) = latest_frame();
|
|
974
1061
|
let latest = Arc::new(Mutex::new(None::<FrameSnapshot>));
|
|
975
1062
|
let observed = latest.clone();
|
|
@@ -1091,6 +1178,8 @@ impl Session {
|
|
|
1091
1178
|
metrics.clone(),
|
|
1092
1179
|
#[cfg(target_os = "linux")]
|
|
1093
1180
|
cursor_sink,
|
|
1181
|
+
#[cfg(target_os = "linux")]
|
|
1182
|
+
indicator.as_ref().map(crate::indicator::Indicator::cues),
|
|
1094
1183
|
sink,
|
|
1095
1184
|
Box::new({
|
|
1096
1185
|
let status = capture_status.clone();
|
|
@@ -1139,23 +1228,13 @@ impl Session {
|
|
|
1139
1228
|
let (width, height) =
|
|
1140
1229
|
fit(source_w, source_h, request.max_width, request.max_height);
|
|
1141
1230
|
#[cfg(target_os = "linux")]
|
|
1142
|
-
if request.agent_indicator
|
|
1143
|
-
indicator =
|
|
1144
|
-
crate::indicator::Indicator::start_wayland(source_w, source_h).map_err(
|
|
1145
|
-
|error| {
|
|
1146
|
-
SessionError::new("indicator-unavailable", format!("{error:#}"))
|
|
1147
|
-
},
|
|
1148
|
-
)?,
|
|
1149
|
-
);
|
|
1231
|
+
if request.agent_indicator {
|
|
1232
|
+
indicator = crate::indicator::Indicator::start_wayland(source_w, source_h).ok();
|
|
1150
1233
|
}
|
|
1151
1234
|
#[cfg(target_os = "macos")]
|
|
1152
|
-
if request.agent_indicator
|
|
1153
|
-
indicator =
|
|
1154
|
-
crate::indicator::Indicator::start(source.node_id, source_w, source_h)
|
|
1155
|
-
.map_err(|error| {
|
|
1156
|
-
SessionError::new("indicator", format!("{error:#}"))
|
|
1157
|
-
})?,
|
|
1158
|
-
);
|
|
1235
|
+
if request.agent_indicator {
|
|
1236
|
+
indicator =
|
|
1237
|
+
crate::indicator::Indicator::start(source.node_id, source_w, source_h).ok();
|
|
1159
1238
|
}
|
|
1160
1239
|
#[cfg(target_os = "macos")]
|
|
1161
1240
|
let capture = capture::start(
|
|
@@ -1230,20 +1309,6 @@ impl Session {
|
|
|
1230
1309
|
|
|
1231
1310
|
let source_w = source.width.max(1) as usize;
|
|
1232
1311
|
let source_h = source.height.max(1) as usize;
|
|
1233
|
-
#[cfg(target_os = "linux")]
|
|
1234
|
-
if request.agent_indicator && wants_control && wants_x11 {
|
|
1235
|
-
let display = match &request.source {
|
|
1236
|
-
Some(SourceRequest::X11 {
|
|
1237
|
-
display: Some(display),
|
|
1238
|
-
}) => display.clone(),
|
|
1239
|
-
_ => std::env::var("DISPLAY")
|
|
1240
|
-
.map_err(|_| SessionError::new("indicator-unavailable", "no X display"))?,
|
|
1241
|
-
};
|
|
1242
|
-
indicator = Some(
|
|
1243
|
-
crate::indicator::Indicator::start(&display, source_w, source_h)
|
|
1244
|
-
.map_err(|error| SessionError::new("indicator", format!("{error:#}")))?,
|
|
1245
|
-
);
|
|
1246
|
-
}
|
|
1247
1312
|
let (width, height) = fit(source_w, source_h, request.max_width, request.max_height);
|
|
1248
1313
|
|
|
1249
1314
|
let bitrate_kbps = if request.bitrate_kbps == 0 {
|
|
@@ -1262,40 +1327,30 @@ impl Session {
|
|
|
1262
1327
|
|
|
1263
1328
|
// Everything that can still refuse the session comes before the peer:
|
|
1264
1329
|
// a peer created and then abandoned keeps its socket and tasks alive.
|
|
1265
|
-
let
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
let devices = InputDevices::create(source_w as i32, source_h as i32);
|
|
1281
|
-
#[cfg(any(target_os = "macos", target_os = "windows"))]
|
|
1282
|
-
let devices = InputDevices::create_for_display(
|
|
1283
|
-
source_w as i32,
|
|
1284
|
-
source_h as i32,
|
|
1285
|
-
source.node_id,
|
|
1286
|
-
);
|
|
1287
|
-
devices.map_err(|error| {
|
|
1288
|
-
SessionError::new("input-unavailable", format!("{error:#}"))
|
|
1289
|
-
})?
|
|
1290
|
-
}),
|
|
1291
|
-
held: HeldState::default(),
|
|
1292
|
-
explicit_modifiers: Vec::new(),
|
|
1293
|
-
chord_modifiers: Vec::new(),
|
|
1294
|
-
chord_keys: Vec::new(),
|
|
1295
|
-
wheel_rest: (0.0, 0.0),
|
|
1296
|
-
},
|
|
1297
|
-
})
|
|
1330
|
+
let applier = match &x11 {
|
|
1331
|
+
Some(desktop) => lock(desktop)
|
|
1332
|
+
.input_available()
|
|
1333
|
+
.then(|| Applier::X11(desktop.clone())),
|
|
1334
|
+
None => {
|
|
1335
|
+
#[cfg(target_os = "linux")]
|
|
1336
|
+
let devices = InputDevices::create(source_w as i32, source_h as i32);
|
|
1337
|
+
#[cfg(any(target_os = "macos", target_os = "windows"))]
|
|
1338
|
+
let devices = InputDevices::create_for_display(
|
|
1339
|
+
source_w as i32,
|
|
1340
|
+
source_h as i32,
|
|
1341
|
+
source.node_id,
|
|
1342
|
+
);
|
|
1343
|
+
devices.ok().map(Applier::Uinput)
|
|
1344
|
+
}
|
|
1298
1345
|
};
|
|
1346
|
+
let input = applier.map(|applier| InputTarget {
|
|
1347
|
+
applier,
|
|
1348
|
+
held: HeldState::default(),
|
|
1349
|
+
explicit_modifiers: Vec::new(),
|
|
1350
|
+
chord_modifiers: Vec::new(),
|
|
1351
|
+
chord_keys: Vec::new(),
|
|
1352
|
+
wheel_rest: (0.0, 0.0),
|
|
1353
|
+
});
|
|
1299
1354
|
|
|
1300
1355
|
let layout = Layout::from_environment().map_err(|error| {
|
|
1301
1356
|
SessionError::new("input", format!("no keyboard layout: {error:#}"))
|
|
@@ -1338,7 +1393,6 @@ impl Session {
|
|
|
1338
1393
|
let inner = Arc::new(Inner {
|
|
1339
1394
|
id,
|
|
1340
1395
|
generation,
|
|
1341
|
-
permissions: request.permissions,
|
|
1342
1396
|
source,
|
|
1343
1397
|
geometry,
|
|
1344
1398
|
metrics,
|
|
@@ -1432,7 +1486,6 @@ impl Session {
|
|
|
1432
1486
|
let inner = Arc::new(Inner {
|
|
1433
1487
|
id,
|
|
1434
1488
|
generation,
|
|
1435
|
-
permissions: request.permissions,
|
|
1436
1489
|
source: SelectedSource {
|
|
1437
1490
|
node_id: 0,
|
|
1438
1491
|
width: width as i32,
|
|
@@ -1532,6 +1585,10 @@ impl Session {
|
|
|
1532
1585
|
self.inner.generation
|
|
1533
1586
|
}
|
|
1534
1587
|
|
|
1588
|
+
pub fn agent_indicator(&self) -> bool {
|
|
1589
|
+
lock(&self.inner.indicator).is_some()
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1535
1592
|
pub fn cursor_info(&self) -> &serde_json::Value {
|
|
1536
1593
|
&self.inner.cursor_info
|
|
1537
1594
|
}
|
|
@@ -1554,12 +1611,6 @@ impl Session {
|
|
|
1554
1611
|
if self.inner.closed.load(Ordering::SeqCst) {
|
|
1555
1612
|
return Err(SessionError::new("session", "the session is closed"));
|
|
1556
1613
|
}
|
|
1557
|
-
if !self.inner.permissions.contains(&Permission::View) {
|
|
1558
|
-
return Err(SessionError::new(
|
|
1559
|
-
"permission",
|
|
1560
|
-
"point requires view permission",
|
|
1561
|
-
));
|
|
1562
|
-
}
|
|
1563
1614
|
if lock(&self.inner.encoded).is_some() {
|
|
1564
1615
|
return Err(SessionError::new(
|
|
1565
1616
|
"operation",
|
|
@@ -1732,12 +1783,6 @@ impl Session {
|
|
|
1732
1783
|
path: &str,
|
|
1733
1784
|
region: Option<[usize; 4]>,
|
|
1734
1785
|
) -> std::result::Result<serde_json::Value, SessionError> {
|
|
1735
|
-
if !self.inner.permissions.contains(&Permission::View) {
|
|
1736
|
-
return Err(SessionError::new(
|
|
1737
|
-
"permission",
|
|
1738
|
-
"view permission is required",
|
|
1739
|
-
));
|
|
1740
|
-
}
|
|
1741
1786
|
if lock(&self.inner.encoded).is_some() {
|
|
1742
1787
|
return Err(SessionError::new(
|
|
1743
1788
|
"operation",
|
|
@@ -1843,20 +1888,18 @@ impl Session {
|
|
|
1843
1888
|
if let Some((_, reason)) = self.inner.clipboard_refusal() {
|
|
1844
1889
|
return Err(reason.to_owned());
|
|
1845
1890
|
}
|
|
1846
|
-
|
|
1891
|
+
let inner = self.inner.clone();
|
|
1892
|
+
clipboard_task(move || inner.clipboard_read_text()).await
|
|
1847
1893
|
}
|
|
1848
1894
|
|
|
1849
1895
|
pub async fn write_clipboard(&self, text: String) -> std::result::Result<(), String> {
|
|
1850
1896
|
if let Some((_, reason)) = self.inner.clipboard_refusal() {
|
|
1851
1897
|
return Err(reason.to_owned());
|
|
1852
1898
|
}
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
})
|
|
1858
|
-
.await
|
|
1859
|
-
.map(|_written| ())
|
|
1899
|
+
let inner = self.inner.clone();
|
|
1900
|
+
clipboard_task(move || inner.clipboard_write_text(&text))
|
|
1901
|
+
.await
|
|
1902
|
+
.map(|_written| ())
|
|
1860
1903
|
}
|
|
1861
1904
|
|
|
1862
1905
|
pub fn mark_closed_reason(&self) -> Option<String> {
|
|
@@ -1945,24 +1988,40 @@ impl Inner {
|
|
|
1945
1988
|
}
|
|
1946
1989
|
}
|
|
1947
1990
|
|
|
1948
|
-
fn
|
|
1949
|
-
|
|
1991
|
+
fn clipboard_read_text(&self) -> std::result::Result<(String, bool), String> {
|
|
1992
|
+
#[cfg(target_os = "linux")]
|
|
1993
|
+
if let PointDesktop::X11(display) = &self.point_desktop {
|
|
1994
|
+
return clipboard::x11_read(display).map_err(|error| format!("{error:#}"));
|
|
1995
|
+
}
|
|
1996
|
+
clipboard::read_or_explain()
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
fn clipboard_write_text(&self, text: &str) -> std::result::Result<String, String> {
|
|
2000
|
+
#[cfg(target_os = "linux")]
|
|
2001
|
+
if let PointDesktop::X11(display) = &self.point_desktop {
|
|
2002
|
+
return clipboard::x11_write(display, text)
|
|
2003
|
+
.map(|()| String::new())
|
|
2004
|
+
.map_err(|error| format!("{error:#}"));
|
|
2005
|
+
}
|
|
2006
|
+
clipboard::write(text)
|
|
2007
|
+
.map(|()| String::new())
|
|
2008
|
+
.map_err(|error| format!("{error:#}"))
|
|
1950
2009
|
}
|
|
1951
2010
|
|
|
1952
2011
|
fn clipboard_refusal(&self) -> Option<(&'static str, &'static str)> {
|
|
1953
|
-
if
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
{
|
|
2012
|
+
if self.closed.load(Ordering::Relaxed) {
|
|
2013
|
+
return Some(("session", "the session has ended"));
|
|
2014
|
+
}
|
|
2015
|
+
#[cfg(target_os = "linux")]
|
|
2016
|
+
if self.source.source_type.as_deref() == Some("x11-root") {
|
|
2017
|
+
return None;
|
|
2018
|
+
}
|
|
2019
|
+
if self.source.source_type.as_deref() == Some("encoded") || !wayland_clipboard_available() {
|
|
1958
2020
|
return Some((
|
|
1959
2021
|
"clipboard-unsupported",
|
|
1960
2022
|
"clipboard is unavailable for this desktop source",
|
|
1961
2023
|
));
|
|
1962
2024
|
}
|
|
1963
|
-
if !self.has(Permission::Clipboard) {
|
|
1964
|
-
return Some(("permission", "this session has no clipboard permission"));
|
|
1965
|
-
}
|
|
1966
2025
|
None
|
|
1967
2026
|
}
|
|
1968
2027
|
|
|
@@ -1990,7 +2049,7 @@ impl Inner {
|
|
|
1990
2049
|
Ok(())
|
|
1991
2050
|
}
|
|
1992
2051
|
|
|
1993
|
-
/// Admit one client action. Validation
|
|
2052
|
+
/// Admit one client action. Validation and ordering checks all
|
|
1994
2053
|
/// happen before any physical effect, so a refused action leaves the desktop
|
|
1995
2054
|
/// exactly as it was.
|
|
1996
2055
|
fn apply(self: &Arc<Self>, message: ControlMessage) {
|
|
@@ -2003,10 +2062,6 @@ impl Inner {
|
|
|
2003
2062
|
self.reject(seq, "session", "the control channel is not open");
|
|
2004
2063
|
return;
|
|
2005
2064
|
}
|
|
2006
|
-
if !self.has(Permission::Control) {
|
|
2007
|
-
self.reject(seq, "permission", "this session is view-only");
|
|
2008
|
-
return;
|
|
2009
|
-
}
|
|
2010
2065
|
if seq != 0 {
|
|
2011
2066
|
let mut last = match self.last_seq.lock() {
|
|
2012
2067
|
Ok(last) => last,
|
|
@@ -2043,6 +2098,14 @@ impl Inner {
|
|
|
2043
2098
|
self.forward(message);
|
|
2044
2099
|
return;
|
|
2045
2100
|
}
|
|
2101
|
+
if !matches!(
|
|
2102
|
+
&message,
|
|
2103
|
+
ControlMessage::ClipboardRead { .. } | ControlMessage::ClipboardWrite { .. }
|
|
2104
|
+
) && lock(&self.input).is_none()
|
|
2105
|
+
{
|
|
2106
|
+
self.reject(seq, "input-unavailable", "no input backend");
|
|
2107
|
+
return;
|
|
2108
|
+
}
|
|
2046
2109
|
let outcome = match message {
|
|
2047
2110
|
ControlMessage::Pointer {
|
|
2048
2111
|
phase,
|
|
@@ -2318,10 +2381,12 @@ impl Inner {
|
|
|
2318
2381
|
}
|
|
2319
2382
|
let inner = Arc::clone(self);
|
|
2320
2383
|
tokio::spawn(async move {
|
|
2321
|
-
let
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2384
|
+
let reader = inner.clone();
|
|
2385
|
+
let (text, truncated, error) =
|
|
2386
|
+
match clipboard_task(move || reader.clipboard_read_text()).await {
|
|
2387
|
+
Ok((text, truncated)) => (text, truncated, None),
|
|
2388
|
+
Err(reason) => (String::new(), false, Some(reason)),
|
|
2389
|
+
};
|
|
2325
2390
|
let _ = inner.reply(&ControlReply::Clipboard {
|
|
2326
2391
|
request: &request,
|
|
2327
2392
|
text,
|
|
@@ -2338,13 +2403,10 @@ impl Inner {
|
|
|
2338
2403
|
}
|
|
2339
2404
|
let inner = Arc::clone(self);
|
|
2340
2405
|
tokio::spawn(async move {
|
|
2341
|
-
let
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
})
|
|
2346
|
-
.await
|
|
2347
|
-
.err();
|
|
2406
|
+
let writer = inner.clone();
|
|
2407
|
+
let error = clipboard_task(move || writer.clipboard_write_text(&text))
|
|
2408
|
+
.await
|
|
2409
|
+
.err();
|
|
2348
2410
|
let _ = inner.reply(&ControlReply::Clipboard {
|
|
2349
2411
|
request: &request,
|
|
2350
2412
|
text: String::new(),
|
|
@@ -3216,6 +3278,60 @@ fn opaque_id() -> String {
|
|
|
3216
3278
|
|
|
3217
3279
|
#[cfg(test)]
|
|
3218
3280
|
mod tests {
|
|
3281
|
+
#[cfg(target_os = "linux")]
|
|
3282
|
+
#[test]
|
|
3283
|
+
fn x11_cue_mask_restores_only_the_cue_pixels() {
|
|
3284
|
+
use crate::indicator::Cue;
|
|
3285
|
+
let (w, h) = (200usize, 120usize);
|
|
3286
|
+
let at = |x: usize, y: usize| (y * w + x) * 4;
|
|
3287
|
+
let (cx, cy) = (20 + 55, 10 + 55);
|
|
3288
|
+
// Shaped (128) and composited (1) helpers announce different thresholds.
|
|
3289
|
+
for threshold in [128, 1] {
|
|
3290
|
+
let cue = Cue {
|
|
3291
|
+
x: 20,
|
|
3292
|
+
y: 10,
|
|
3293
|
+
size: 110,
|
|
3294
|
+
unit: 1.0,
|
|
3295
|
+
opacity: 1.0,
|
|
3296
|
+
click: -1.0,
|
|
3297
|
+
typed: -1.0,
|
|
3298
|
+
threshold,
|
|
3299
|
+
reveal: 99.0,
|
|
3300
|
+
};
|
|
3301
|
+
let mut mask = CueMask::new(Default::default());
|
|
3302
|
+
let clean: Vec<u8> = (0..w * h * 4).map(|i| (i % 251) as u8).collect();
|
|
3303
|
+
mask.apply(&mut clean.clone(), w, h, vec![]);
|
|
3304
|
+
// Paint the cue's keyline and live content inside the halo.
|
|
3305
|
+
let mut frame = clean.clone();
|
|
3306
|
+
frame[at(cx - 24, cy)..at(cx - 24, cy) + 4].fill(255);
|
|
3307
|
+
frame[at(cx - 5, cy)..at(cx - 5, cy) + 4].fill(7);
|
|
3308
|
+
frame[at(190, 110)..at(190, 110) + 4].fill(9);
|
|
3309
|
+
mask.apply(&mut frame, w, h, vec![cue]);
|
|
3310
|
+
assert_eq!(
|
|
3311
|
+
&frame[at(cx - 24, cy)..at(cx - 24, cy) + 4],
|
|
3312
|
+
&clean[at(cx - 24, cy)..at(cx - 24, cy) + 4]
|
|
3313
|
+
);
|
|
3314
|
+
assert_eq!(
|
|
3315
|
+
&frame[at(cx - 5, cy)..at(cx - 5, cy) + 4],
|
|
3316
|
+
&[7; 4],
|
|
3317
|
+
"the halo interior stays live"
|
|
3318
|
+
);
|
|
3319
|
+
assert_eq!(
|
|
3320
|
+
&frame[at(190, 110)..at(190, 110) + 4],
|
|
3321
|
+
&[9; 4],
|
|
3322
|
+
"pixels away from the cue stay live"
|
|
3323
|
+
);
|
|
3324
|
+
let mut after = clean.clone();
|
|
3325
|
+
after[at(cx - 24, cy)..at(cx - 24, cy) + 4].fill(3);
|
|
3326
|
+
mask.apply(&mut after, w, h, vec![]);
|
|
3327
|
+
assert_eq!(
|
|
3328
|
+
&after[at(cx - 24, cy)..at(cx - 24, cy) + 4],
|
|
3329
|
+
&[3; 4],
|
|
3330
|
+
"a hidden cue masks nothing"
|
|
3331
|
+
);
|
|
3332
|
+
}
|
|
3333
|
+
}
|
|
3334
|
+
|
|
3219
3335
|
use super::*;
|
|
3220
3336
|
|
|
3221
3337
|
#[tokio::test]
|
|
@@ -3226,7 +3342,6 @@ mod tests {
|
|
|
3226
3342
|
let inner = Arc::new(Inner {
|
|
3227
3343
|
id: String::from("encoded-session"),
|
|
3228
3344
|
generation: 1,
|
|
3229
|
-
permissions: Vec::new(),
|
|
3230
3345
|
source: SelectedSource {
|
|
3231
3346
|
node_id: 0,
|
|
3232
3347
|
width: 640,
|
|
@@ -3396,10 +3511,7 @@ mod tests {
|
|
|
3396
3511
|
#[tokio::test]
|
|
3397
3512
|
async fn a_wayland_portal_session_reports_typed_point_unsupported() {
|
|
3398
3513
|
let (events, _) = tokio_mpsc::unbounded_channel();
|
|
3399
|
-
let (
|
|
3400
|
-
Arc::get_mut(&mut inner)
|
|
3401
|
-
.expect("test session has one owner")
|
|
3402
|
-
.permissions = vec![Permission::View];
|
|
3514
|
+
let (inner, _) = test_inner(events).await;
|
|
3403
3515
|
let session = Session { inner };
|
|
3404
3516
|
|
|
3405
3517
|
let error = session
|
|
@@ -3445,7 +3557,6 @@ mod tests {
|
|
|
3445
3557
|
let (events, _) = tokio_mpsc::unbounded_channel();
|
|
3446
3558
|
let (mut inner, _) = test_inner(events).await;
|
|
3447
3559
|
let state = Arc::get_mut(&mut inner).unwrap();
|
|
3448
|
-
state.permissions = vec![Permission::View];
|
|
3449
3560
|
state.point_desktop = PointDesktop::Wayland;
|
|
3450
3561
|
state.source.width = 1280;
|
|
3451
3562
|
state.source.height = 720;
|
|
@@ -3538,7 +3649,6 @@ mod tests {
|
|
|
3538
3649
|
let inner = Arc::new(Inner {
|
|
3539
3650
|
id: String::from("test-session"),
|
|
3540
3651
|
generation: 1,
|
|
3541
|
-
permissions: vec![Permission::Control],
|
|
3542
3652
|
source: SelectedSource {
|
|
3543
3653
|
node_id: 0,
|
|
3544
3654
|
width: 640,
|
|
@@ -3587,6 +3697,36 @@ mod tests {
|
|
|
3587
3697
|
(inner, recorded)
|
|
3588
3698
|
}
|
|
3589
3699
|
|
|
3700
|
+
#[tokio::test]
|
|
3701
|
+
async fn paired_input_without_a_backend_is_an_action_failure() {
|
|
3702
|
+
let (events, _) = tokio_mpsc::unbounded_channel();
|
|
3703
|
+
let (inner, recorded) = test_inner(events).await;
|
|
3704
|
+
*lock(&inner.input) = None;
|
|
3705
|
+
assert_eq!(
|
|
3706
|
+
inner.with_input(|_| Ok(())).unwrap_err().0,
|
|
3707
|
+
"input-unavailable"
|
|
3708
|
+
);
|
|
3709
|
+
inner.apply(ControlMessage::Pointer {
|
|
3710
|
+
phase: PointerPhase::Move,
|
|
3711
|
+
x: 10,
|
|
3712
|
+
y: 20,
|
|
3713
|
+
button: 1,
|
|
3714
|
+
seq: 1,
|
|
3715
|
+
});
|
|
3716
|
+
assert_eq!(
|
|
3717
|
+
*lock(&inner.last_seq),
|
|
3718
|
+
1,
|
|
3719
|
+
"paired action passes admission, never a permission refusal"
|
|
3720
|
+
);
|
|
3721
|
+
assert_eq!(lock(&inner.metrics).input_rejected, 1);
|
|
3722
|
+
assert_eq!(lock(&inner.metrics).input_applied, 0);
|
|
3723
|
+
assert!(lock(&recorded).is_empty());
|
|
3724
|
+
assert!(
|
|
3725
|
+
!inner.closed.load(Ordering::Relaxed),
|
|
3726
|
+
"missing input does not close view access"
|
|
3727
|
+
);
|
|
3728
|
+
}
|
|
3729
|
+
|
|
3590
3730
|
#[tokio::test]
|
|
3591
3731
|
async fn a_lease_that_expires_ends_the_session_and_releases_held_input() {
|
|
3592
3732
|
let (events, mut received) = tokio_mpsc::unbounded_channel();
|
|
@@ -3845,38 +3985,24 @@ mod tests {
|
|
|
3845
3985
|
}
|
|
3846
3986
|
|
|
3847
3987
|
#[tokio::test]
|
|
3848
|
-
async fn
|
|
3988
|
+
async fn x11_capture_does_not_refuse_clipboard() {
|
|
3849
3989
|
let (events, _received) = tokio_mpsc::unbounded_channel();
|
|
3850
3990
|
let (mut inner, _recorded) = test_inner(events).await;
|
|
3851
3991
|
let owned = Arc::get_mut(&mut inner).unwrap();
|
|
3852
3992
|
owned.source.source_type = Some(String::from("x11-root"));
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
.unwrap_err()
|
|
3861
|
-
.contains("unavailable"));
|
|
3862
|
-
assert!(session
|
|
3863
|
-
.write_clipboard(String::from("secret"))
|
|
3864
|
-
.await
|
|
3865
|
-
.unwrap_err()
|
|
3866
|
-
.contains("unavailable"));
|
|
3867
|
-
inner.clipboard_read(String::from("read"));
|
|
3868
|
-
inner.clipboard_write(String::from("write"), String::from("secret"));
|
|
3869
|
-
assert_eq!(inner.metrics.lock().unwrap().input_rejected, 2);
|
|
3993
|
+
#[cfg(target_os = "linux")]
|
|
3994
|
+
assert_eq!(inner.clipboard_refusal(), None);
|
|
3995
|
+
Arc::get_mut(&mut inner).unwrap().source.source_type = Some(String::from("encoded"));
|
|
3996
|
+
assert_eq!(
|
|
3997
|
+
inner.clipboard_refusal().unwrap().0,
|
|
3998
|
+
"clipboard-unsupported"
|
|
3999
|
+
);
|
|
3870
4000
|
}
|
|
3871
4001
|
|
|
3872
4002
|
#[tokio::test]
|
|
3873
4003
|
async fn a_stopped_capture_never_serves_the_last_frame() {
|
|
3874
4004
|
let (events, _) = tokio_mpsc::unbounded_channel();
|
|
3875
|
-
let (
|
|
3876
|
-
Arc::get_mut(&mut inner)
|
|
3877
|
-
.unwrap()
|
|
3878
|
-
.permissions
|
|
3879
|
-
.push(Permission::View);
|
|
4005
|
+
let (inner, _) = test_inner(events).await;
|
|
3880
4006
|
let session = Session { inner };
|
|
3881
4007
|
*lock(&session.inner.latest) =
|
|
3882
4008
|
Some((1, Instant::now(), 2, 2, vec![0; 16], vec![1], vec![]));
|
|
@@ -3956,7 +4082,6 @@ mod tests {
|
|
|
3956
4082
|
width: 486,
|
|
3957
4083
|
height: 1080,
|
|
3958
4084
|
}),
|
|
3959
|
-
permissions: vec![Permission::View, Permission::Control],
|
|
3960
4085
|
max_width: 3840,
|
|
3961
4086
|
max_height: 2160,
|
|
3962
4087
|
bitrate_kbps: 0,
|
|
@@ -4130,35 +4255,6 @@ mod tests {
|
|
|
4130
4255
|
);
|
|
4131
4256
|
}
|
|
4132
4257
|
|
|
4133
|
-
#[tokio::test]
|
|
4134
|
-
async fn opening_without_view_permission_is_refused_before_capture() {
|
|
4135
|
-
let (events, _received) = tokio_mpsc::unbounded_channel();
|
|
4136
|
-
let result = Session::open(
|
|
4137
|
-
OpenRequest {
|
|
4138
|
-
cursor: crate::protocol::CursorMode::Embedded,
|
|
4139
|
-
source: None,
|
|
4140
|
-
permissions: vec![Permission::Control],
|
|
4141
|
-
max_width: 640,
|
|
4142
|
-
max_height: 480,
|
|
4143
|
-
bitrate_kbps: 0,
|
|
4144
|
-
max_fps: 30,
|
|
4145
|
-
ice_servers: Vec::new(),
|
|
4146
|
-
restore_token: None,
|
|
4147
|
-
ttl: None,
|
|
4148
|
-
loopback_tcp: false,
|
|
4149
|
-
agent_indicator: false,
|
|
4150
|
-
local_frames: true,
|
|
4151
|
-
},
|
|
4152
|
-
events,
|
|
4153
|
-
)
|
|
4154
|
-
.await;
|
|
4155
|
-
let error = match result {
|
|
4156
|
-
Ok(_) => panic!("a viewless session must not open"),
|
|
4157
|
-
Err(error) => error,
|
|
4158
|
-
};
|
|
4159
|
-
assert_eq!(error.code, "permission");
|
|
4160
|
-
}
|
|
4161
|
-
|
|
4162
4258
|
#[tokio::test]
|
|
4163
4259
|
async fn a_lost_transport_releases_input_then_ends_the_session_without_sctp_close() {
|
|
4164
4260
|
use webrtc::peer_connection::RTCPeerConnectionState as State;
|