@desklink/host 0.5.2 → 0.5.4
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 +45 -18
- package/bin/desklink-host.mjs +5 -1
- package/dist/bridge.d.ts +7 -0
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js +55 -7
- package/dist/bridge.js.map +1 -1
- package/dist/engineProcess.d.ts +9 -2
- package/dist/engineProcess.d.ts.map +1 -1
- package/dist/engineProcess.js +26 -15
- package/dist/engineProcess.js.map +1 -1
- package/dist/protocol.d.ts +1 -1
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +1 -1
- package/dist/protocol.js.map +1 -1
- package/docs/BOUNDARY.md +4 -1
- package/docs/PROTOCOL.md +20 -5
- package/engine/Cargo.lock +6 -8
- package/engine/Cargo.toml +13 -1
- package/engine/src/clipboard.rs +0 -34
- package/engine/src/convert.rs +0 -170
- package/engine/src/cursor.rs +0 -23
- package/engine/src/input.rs +0 -65
- package/engine/src/input_mac.rs +0 -11
- package/engine/src/keeper.rs +0 -137
- package/engine/src/keymap.rs +0 -107
- package/engine/src/keymap_mac.rs +0 -25
- package/engine/src/mac.rs +0 -28
- package/engine/src/main.rs +3 -2
- package/engine/src/peer.rs +231 -1
- package/engine/src/portal.rs +0 -18
- package/engine/src/protocol.rs +0 -162
- package/engine/src/session.rs +37 -1
- package/engine/src/x11.rs +0 -13
- package/engine/vendor/rtc-ice/Cargo.toml +137 -0
- package/engine/vendor/rtc-ice/DESKLINK-PATCH.md +59 -0
- package/engine/vendor/rtc-ice/LICENSE-APACHE +201 -0
- package/engine/vendor/rtc-ice/LICENSE-MIT +21 -0
- package/engine/vendor/rtc-ice/README.md +30 -0
- package/engine/vendor/rtc-ice/examples/ping_pong.rs +376 -0
- package/engine/vendor/rtc-ice/src/agent/agent_config.rs +126 -0
- package/engine/vendor/rtc-ice/src/agent/agent_proto.rs +172 -0
- package/engine/vendor/rtc-ice/src/agent/agent_selector.rs +566 -0
- package/engine/vendor/rtc-ice/src/agent/agent_stats.rs +292 -0
- package/engine/vendor/rtc-ice/src/agent/agent_test.rs +3388 -0
- package/engine/vendor/rtc-ice/src/agent/mod.rs +1662 -0
- package/engine/vendor/rtc-ice/src/attributes/control/control_test.rs +168 -0
- package/engine/vendor/rtc-ice/src/attributes/control/mod.rs +142 -0
- package/engine/vendor/rtc-ice/src/attributes/mod.rs +6 -0
- package/engine/vendor/rtc-ice/src/attributes/priority/mod.rs +37 -0
- package/engine/vendor/rtc-ice/src/attributes/priority/priority_test.rs +39 -0
- package/engine/vendor/rtc-ice/src/attributes/use_candidate/mod.rs +33 -0
- package/engine/vendor/rtc-ice/src/attributes/use_candidate/use_candidate_test.rs +19 -0
- package/engine/vendor/rtc-ice/src/candidate/candidate_host.rs +53 -0
- package/engine/vendor/rtc-ice/src/candidate/candidate_pair.rs +215 -0
- package/engine/vendor/rtc-ice/src/candidate/candidate_pair_test.rs +201 -0
- package/engine/vendor/rtc-ice/src/candidate/candidate_peer_reflexive.rs +49 -0
- package/engine/vendor/rtc-ice/src/candidate/candidate_relay.rs +53 -0
- package/engine/vendor/rtc-ice/src/candidate/candidate_relay_test.rs +114 -0
- package/engine/vendor/rtc-ice/src/candidate/candidate_server_reflexive.rs +53 -0
- package/engine/vendor/rtc-ice/src/candidate/candidate_server_reflexive_test.rs +91 -0
- package/engine/vendor/rtc-ice/src/candidate/candidate_test.rs +540 -0
- package/engine/vendor/rtc-ice/src/candidate/mod.rs +679 -0
- package/engine/vendor/rtc-ice/src/lib.rs +85 -0
- package/engine/vendor/rtc-ice/src/mdns/mdns_test.rs +150 -0
- package/engine/vendor/rtc-ice/src/mdns/mod.rs +69 -0
- package/engine/vendor/rtc-ice/src/network_type/mod.rs +162 -0
- package/engine/vendor/rtc-ice/src/network_type/network_type_test.rs +95 -0
- package/engine/vendor/rtc-ice/src/rand/mod.rs +91 -0
- package/engine/vendor/rtc-ice/src/rand/rand_test.rs +115 -0
- package/engine/vendor/rtc-ice/src/state/mod.rs +106 -0
- package/engine/vendor/rtc-ice/src/state/state_test.rs +46 -0
- package/engine/vendor/rtc-ice/src/stats/mod.rs +166 -0
- package/engine/vendor/rtc-ice/src/tcp_type/mod.rs +47 -0
- package/engine/vendor/rtc-ice/src/tcp_type/tcp_type_test.rs +18 -0
- package/engine/vendor/rtc-ice/src/url/mod.rs +266 -0
- package/engine/vendor/rtc-ice/src/url/url_test.rs +142 -0
- package/package.json +8 -3
package/docs/BOUNDARY.md
CHANGED
|
@@ -141,7 +141,10 @@ address, port, code or certificate reaches the user, and the engine's session is
|
|
|
141
141
|
started by the host that already serves the terminal. A consumer with no channel
|
|
142
142
|
of its own is not left to invent one — `desklink-host bridge` re-serves the same
|
|
143
143
|
protocol over a WebSocket and serves a reference client that needs no build step,
|
|
144
|
-
so a stranger's first run is one command and one URL.
|
|
144
|
+
so a stranger's first run is one command and one URL. Its bounded reattach window
|
|
145
|
+
is in-memory only: no durable store, new pairing authority, lease renewal or
|
|
146
|
+
restart-after-process-exit guarantee. Consumers close revoked pairing sessions
|
|
147
|
+
explicitly; a missing signaling carrier is not itself revocation.
|
|
145
148
|
|
|
146
149
|
The same substitution is therefore available to both: the packages carry no muxr
|
|
147
150
|
concept, and muxr's contribution is one file that adapts its own channel to the
|
package/docs/PROTOCOL.md
CHANGED
|
@@ -346,7 +346,16 @@ re-offers — a fresh offer with a new ICE generation — as a new
|
|
|
346
346
|
`session.description` event, which the client answers as usual. The session
|
|
347
347
|
and its generation survive; only the ICE generation turns over. Fresh
|
|
348
348
|
candidates from the restart trickle through `session.candidate` on both sides
|
|
349
|
-
exactly as the initial ones do.
|
|
349
|
+
exactly as the initial ones do. If a local offer is still unanswered, the
|
|
350
|
+
engine re-emits that pending offer instead: queued restarts share its ICE
|
|
351
|
+
credentials until the answer arrives.
|
|
352
|
+
|
|
353
|
+
The WebSocket bridge retains the session after its last signaling socket leaves
|
|
354
|
+
for `reattachMs` (default 30000 ms; CLI `--reattach-ms`; 0 closes immediately).
|
|
355
|
+
A token-authenticated socket attaching within that window cancels the deadline;
|
|
356
|
+
it may restart ICE with the held session id without a new `session.open`.
|
|
357
|
+
This does not replay offers or automatically reconnect a client, extend its
|
|
358
|
+
lease, or guarantee recovery within 2 seconds. Engine-side ends remain terminal.
|
|
350
359
|
|
|
351
360
|
```jsonc
|
|
352
361
|
{"id":7,"method":"session.restart_ice","params":{"session_id":"…","generation":1}}
|
|
@@ -421,9 +430,13 @@ The session ended on the engine's side; there is nothing further to drain.
|
|
|
421
430
|
| `error` | The engine could not keep serving it, such as an encoder refusing a frame | end and report |
|
|
422
431
|
|
|
423
432
|
A session the consumer closes or replaces (`session.close`, a new
|
|
424
|
-
`session.open`, the
|
|
425
|
-
|
|
426
|
-
its control channel is told, with code `closed` (below).
|
|
433
|
+
`session.open`, engine stdin EOF, or the bridge's detach deadline) sends no
|
|
434
|
+
`session.revoked`: signaling sees `session.state` with `capture` `ended`. Only
|
|
435
|
+
its control channel is told, with code `closed` (below). A signaling socket
|
|
436
|
+
leaving alone does not end the session during the bridge's reattach window.
|
|
437
|
+
If the held session is revoked while detached, the bridge remembers its last
|
|
438
|
+
`session.revoked`, replays it once to the next authenticated attacher, then
|
|
439
|
+
forgets it. In particular, a spent lease cannot be revived by reattachment.
|
|
427
440
|
|
|
428
441
|
A code a client does not know is terminal. Engines before this field sent no
|
|
429
442
|
`code`; treat its absence as terminal too.
|
|
@@ -906,4 +919,6 @@ anything to show.
|
|
|
906
919
|
and input; audio is a different product decision.
|
|
907
920
|
- It does not arbitrate between two controllers. One session, one controller.
|
|
908
921
|
- It does not auto-reconnect with authority it no longer holds: after a lease
|
|
909
|
-
expiry or
|
|
922
|
+
expiry or an engine-side end it requires a fresh `session.open`. A brief
|
|
923
|
+
signaling-carrier loss within the bridge's reattach window is not owner
|
|
924
|
+
revocation; the application must close revoked pairing sessions explicitly.
|
package/engine/Cargo.lock
CHANGED
|
@@ -536,7 +536,7 @@ checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
|
|
|
536
536
|
|
|
537
537
|
[[package]]
|
|
538
538
|
name = "desklink-host"
|
|
539
|
-
version = "0.5.
|
|
539
|
+
version = "0.5.4"
|
|
540
540
|
dependencies = [
|
|
541
541
|
"anyhow",
|
|
542
542
|
"ashpd",
|
|
@@ -1942,8 +1942,6 @@ dependencies = [
|
|
|
1942
1942
|
[[package]]
|
|
1943
1943
|
name = "rtc-ice"
|
|
1944
1944
|
version = "0.21.0"
|
|
1945
|
-
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1946
|
-
checksum = "4f437cc58721fdb3f0a2ab8b90660d3c19ea2817bc4e2117b2d80f3f6cfba5f3"
|
|
1947
1945
|
dependencies = [
|
|
1948
1946
|
"bytes",
|
|
1949
1947
|
"crc",
|
|
@@ -2024,9 +2022,9 @@ dependencies = [
|
|
|
2024
2022
|
|
|
2025
2023
|
[[package]]
|
|
2026
2024
|
name = "rtc-sctp"
|
|
2027
|
-
version = "0.21.
|
|
2025
|
+
version = "0.21.1"
|
|
2028
2026
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
2029
|
-
checksum = "
|
|
2027
|
+
checksum = "2eb430b379d90e7984306036d1646454a26b300c9c3256009eb8f7e0615e90b9"
|
|
2030
2028
|
dependencies = [
|
|
2031
2029
|
"bytes",
|
|
2032
2030
|
"crc32c",
|
|
@@ -2051,9 +2049,9 @@ dependencies = [
|
|
|
2051
2049
|
|
|
2052
2050
|
[[package]]
|
|
2053
2051
|
name = "rtc-shared"
|
|
2054
|
-
version = "0.21.
|
|
2052
|
+
version = "0.21.1"
|
|
2055
2053
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
2056
|
-
checksum = "
|
|
2054
|
+
checksum = "1250e81796ac6d3d67bd04ecdb891b8371ff97c8a2ce0a5a1c1321f2e12b657b"
|
|
2057
2055
|
dependencies = [
|
|
2058
2056
|
"bitflags 1.3.2",
|
|
2059
2057
|
"bytes",
|
|
@@ -2426,7 +2424,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
|
2426
2424
|
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
|
2427
2425
|
dependencies = [
|
|
2428
2426
|
"fastrand",
|
|
2429
|
-
"getrandom 0.4
|
|
2427
|
+
"getrandom 0.3.4",
|
|
2430
2428
|
"once_cell",
|
|
2431
2429
|
"rustix",
|
|
2432
2430
|
"windows-sys 0.61.2",
|
package/engine/Cargo.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "desklink-host"
|
|
3
|
-
version = "0.5.
|
|
3
|
+
version = "0.5.4"
|
|
4
4
|
edition = "2021"
|
|
5
5
|
rust-version = "1.97.1"
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -21,6 +21,10 @@ anyhow = "1"
|
|
|
21
21
|
tokio = { version = "1", features = ["rt-multi-thread", "macros", "io-util", "io-std", "sync", "time", "signal"] }
|
|
22
22
|
libc = "0.2"
|
|
23
23
|
bytes = "1"
|
|
24
|
+
# rtc-sctp 0.21.1 is required through this stack: 0.21.0 replaces an
|
|
25
|
+
# expired-but-unhandled T3 retransmission deadline, so the data channel stalls
|
|
26
|
+
# on the new nominated pair after an ICE restart and the client reopens the
|
|
27
|
+
# session. Cargo.lock pins 0.21.1; drop the pin when a later release carries it.
|
|
24
28
|
webrtc = { version = "0.21", default-features = false, features = ["runtime-tokio", "crypto-ring"] }
|
|
25
29
|
rtc = { version = "0.21", default-features = false, features = ["crypto-ring"] }
|
|
26
30
|
log = "0.4"
|
|
@@ -64,3 +68,11 @@ cc = "1"
|
|
|
64
68
|
[profile.release]
|
|
65
69
|
lto = "thin"
|
|
66
70
|
opt-level = 2
|
|
71
|
+
|
|
72
|
+
# The engine needs an ICE restart that does not interrupt media already flowing
|
|
73
|
+
# on the current selected pair (roam persistence, < 200 ms gap). rtc-ice 0.21.0
|
|
74
|
+
# clears the selected pair and every candidate pair in `Agent::apply_restart`,
|
|
75
|
+
# and exposes no setting to keep them, so a patched copy is vendored here. See
|
|
76
|
+
# vendor/rtc-ice/DESKLINK-PATCH.md for what changed and how to drop it.
|
|
77
|
+
[patch.crates-io]
|
|
78
|
+
rtc-ice = { path = "vendor/rtc-ice" }
|
package/engine/src/clipboard.rs
CHANGED
|
@@ -270,37 +270,3 @@ pub fn x11_read(display: &str) -> Result<(String, bool)> {
|
|
|
270
270
|
std::thread::sleep(std::time::Duration::from_millis(5));
|
|
271
271
|
}
|
|
272
272
|
}
|
|
273
|
-
|
|
274
|
-
#[cfg(test)]
|
|
275
|
-
mod tests {
|
|
276
|
-
use super::*;
|
|
277
|
-
|
|
278
|
-
#[test]
|
|
279
|
-
fn an_oversized_clipboard_is_cut_cleanly_and_reported() {
|
|
280
|
-
let mut raw = vec![b'a'; 4];
|
|
281
|
-
raw.extend_from_slice("\u{e9}".as_bytes());
|
|
282
|
-
raw.extend_from_slice(b"zzzz");
|
|
283
|
-
let (text, truncated) =
|
|
284
|
-
bounded_text(&raw, 5).expect("a character-boundary cut is valid text");
|
|
285
|
-
assert!(truncated, "a cut past the limit must be reported");
|
|
286
|
-
assert_eq!(
|
|
287
|
-
text, "aaaa",
|
|
288
|
-
"the partial character is dropped, not invented"
|
|
289
|
-
);
|
|
290
|
-
|
|
291
|
-
// A byte that is not valid UTF-8 before the cut is not a partial
|
|
292
|
-
// character; the whole read is refused.
|
|
293
|
-
let mut invalid = vec![b'a'; 4];
|
|
294
|
-
invalid.push(0xff);
|
|
295
|
-
invalid.extend_from_slice(b"zzzz");
|
|
296
|
-
assert!(bounded_text(&invalid, 5).is_err());
|
|
297
|
-
|
|
298
|
-
let (text, truncated) = bounded_text(b"hello", 5).unwrap();
|
|
299
|
-
assert!(!truncated);
|
|
300
|
-
assert_eq!(text, "hello");
|
|
301
|
-
|
|
302
|
-
let (text, truncated) = bounded_text(b"hello world", 5).unwrap();
|
|
303
|
-
assert!(truncated);
|
|
304
|
-
assert_eq!(text, "hello");
|
|
305
|
-
}
|
|
306
|
-
}
|
package/engine/src/convert.rs
CHANGED
|
@@ -408,173 +408,3 @@ fn pack(
|
|
|
408
408
|
(*u, *v) = chroma(sum(0), sum(1), sum(2));
|
|
409
409
|
}
|
|
410
410
|
}
|
|
411
|
-
|
|
412
|
-
#[cfg(test)]
|
|
413
|
-
mod tests {
|
|
414
|
-
use super::*;
|
|
415
|
-
|
|
416
|
-
#[test]
|
|
417
|
-
fn downscale_keeps_average_colour_and_plane_sizes() {
|
|
418
|
-
// 4x4 opaque red desktop in BGRx, downscaled to 2x2.
|
|
419
|
-
let mut src = vec![0u8; 4 * 16];
|
|
420
|
-
for px in src.chunks_exact_mut(4) {
|
|
421
|
-
px[0] = 0; // B
|
|
422
|
-
px[1] = 0; // G
|
|
423
|
-
px[2] = 255; // R
|
|
424
|
-
px[3] = 255; // X
|
|
425
|
-
}
|
|
426
|
-
let out = to_i420(&src, 4, 4, 16, PixelFormat::Bgrx, 2, 2).unwrap();
|
|
427
|
-
assert_eq!(out.data.len(), 4 + 2 * 1);
|
|
428
|
-
assert!(
|
|
429
|
-
out.y_plane().iter().all(|&y| y > 70),
|
|
430
|
-
"red luma should be high"
|
|
431
|
-
);
|
|
432
|
-
assert!(out.u_plane().iter().all(|&u| u < 128), "red has low Cb");
|
|
433
|
-
assert!(out.v_plane().iter().all(|&v| v > 200), "red has high Cr");
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
#[test]
|
|
437
|
-
fn odd_destination_is_clamped_to_whole_chroma_blocks() {
|
|
438
|
-
let src = vec![255u8; 4 * 16];
|
|
439
|
-
let out = to_i420(&src, 4, 4, 16, PixelFormat::Bgrx, 3, 3).unwrap();
|
|
440
|
-
assert_eq!((out.width, out.height), (2, 2));
|
|
441
|
-
assert_eq!(out.data.len(), 4 + 2 * 1);
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
#[test]
|
|
445
|
-
fn a_stream_a_few_pixels_off_the_encoders_size_is_converted_to_exactly_that_size() {
|
|
446
|
-
// A fractional-scaled display reports one size to the portal and
|
|
447
|
-
// streams another; the encoder only accepts its own.
|
|
448
|
-
let src = vec![200u8; 4 * 1704 * 1066];
|
|
449
|
-
let out = to_i420(&src, 1704, 1066, 1704 * 4, PixelFormat::Bgrx, 1706, 1066).unwrap();
|
|
450
|
-
assert_eq!((out.width, out.height), (1706, 1066));
|
|
451
|
-
let out = to_i420(&src, 1704, 1066, 1704 * 4, PixelFormat::Bgrx, 1702, 1064).unwrap();
|
|
452
|
-
assert_eq!((out.width, out.height), (1702, 1064));
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
#[test]
|
|
456
|
-
fn still_pixels_keep_exact_source_colours_before_encoding() {
|
|
457
|
-
let src = [1, 2, 3, 0, 4, 5, 6, 0];
|
|
458
|
-
assert_eq!(
|
|
459
|
-
to_bgrx(&src, 2, 1, 8, PixelFormat::Bgrx, 2, 1).unwrap(),
|
|
460
|
-
src
|
|
461
|
-
);
|
|
462
|
-
assert_eq!(
|
|
463
|
-
to_bgrx(&src, 2, 1, 8, PixelFormat::Rgbx, 2, 1).unwrap(),
|
|
464
|
-
[3, 2, 1, 255, 6, 5, 4, 255]
|
|
465
|
-
);
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
/// The per-pixel conversion this module used to run, kept as the definition
|
|
469
|
-
/// the fast paths must reproduce bit for bit.
|
|
470
|
-
fn reference(
|
|
471
|
-
src: &[u8],
|
|
472
|
-
sw: usize,
|
|
473
|
-
sh: usize,
|
|
474
|
-
stride: usize,
|
|
475
|
-
format: PixelFormat,
|
|
476
|
-
dw: usize,
|
|
477
|
-
dh: usize,
|
|
478
|
-
) -> Vec<u8> {
|
|
479
|
-
let (bpp, o) = layout(format).unwrap();
|
|
480
|
-
let rgb = |x: usize, y: usize| {
|
|
481
|
-
let p = &src[y * stride + x * bpp..];
|
|
482
|
-
(p[o[0]] as i32, p[o[1]] as i32, p[o[2]] as i32)
|
|
483
|
-
};
|
|
484
|
-
let sample = |dx: usize, dy: usize| {
|
|
485
|
-
let x0 = dx * sw / dw;
|
|
486
|
-
let x1 = ((dx + 1) * sw).div_ceil(dw).clamp(x0 + 1, sw);
|
|
487
|
-
let y0 = dy * sh / dh;
|
|
488
|
-
let y1 = ((dy + 1) * sh).div_ceil(dh).clamp(y0 + 1, sh);
|
|
489
|
-
let (mut r, mut g, mut b) = (0, 0, 0);
|
|
490
|
-
for y in y0..y1 {
|
|
491
|
-
for x in x0..x1 {
|
|
492
|
-
let p = rgb(x, y);
|
|
493
|
-
(r, g, b) = (r + p.0, g + p.1, b + p.2);
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
let n = ((x1 - x0) * (y1 - y0)) as i32;
|
|
497
|
-
(r / n, g / n, b / n)
|
|
498
|
-
};
|
|
499
|
-
let (mut y, mut u, mut v) = (vec![0; dw * dh], vec![], vec![]);
|
|
500
|
-
for cy in 0..dh / 2 {
|
|
501
|
-
for cx in 0..dw / 2 {
|
|
502
|
-
let px = [(0, 0), (1, 0), (0, 1), (1, 1)]
|
|
503
|
-
.map(|(i, j)| (cx * 2 + i, cy * 2 + j, sample(cx * 2 + i, cy * 2 + j)));
|
|
504
|
-
for (x, yy, p) in px {
|
|
505
|
-
y[yy * dw + x] = luma(p.0, p.1, p.2);
|
|
506
|
-
}
|
|
507
|
-
let s = |f: fn(&(i32, i32, i32)) -> i32| {
|
|
508
|
-
(px.iter().map(|p| f(&p.2)).sum::<i32>() + 2) >> 2
|
|
509
|
-
};
|
|
510
|
-
let (cu, cv) = chroma(s(|p| p.0), s(|p| p.1), s(|p| p.2));
|
|
511
|
-
u.push(cu);
|
|
512
|
-
v.push(cv);
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
[y, u, v].concat()
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
#[test]
|
|
519
|
-
fn every_path_matches_the_per_pixel_definition_exactly() {
|
|
520
|
-
let mut seed = 0x9e3779b9u32;
|
|
521
|
-
let mut noise = |n: usize| {
|
|
522
|
-
(0..n)
|
|
523
|
-
.map(|_| {
|
|
524
|
-
seed ^= seed << 13;
|
|
525
|
-
seed ^= seed >> 17;
|
|
526
|
-
seed ^= seed << 5;
|
|
527
|
-
seed as u8
|
|
528
|
-
})
|
|
529
|
-
.collect::<Vec<u8>>()
|
|
530
|
-
};
|
|
531
|
-
let formats = [
|
|
532
|
-
PixelFormat::Bgrx,
|
|
533
|
-
PixelFormat::Rgba,
|
|
534
|
-
PixelFormat::Bgr,
|
|
535
|
-
PixelFormat::Rgb,
|
|
536
|
-
];
|
|
537
|
-
// Native, a few pixels off, box downscales, and boxes near and past the
|
|
538
|
-
// largest area the reciprocal table covers.
|
|
539
|
-
for (sw, sh, dw, dh) in [
|
|
540
|
-
(256, 126, 4, 2),
|
|
541
|
-
(400, 200, 4, 2),
|
|
542
|
-
(64, 36, 64, 36),
|
|
543
|
-
(67, 41, 66, 40),
|
|
544
|
-
(100, 60, 38, 22),
|
|
545
|
-
(90, 50, 92, 50),
|
|
546
|
-
(33, 17, 8, 6),
|
|
547
|
-
] {
|
|
548
|
-
for format in formats {
|
|
549
|
-
let bpp = layout(format).unwrap().0;
|
|
550
|
-
let stride = sw * bpp + 12;
|
|
551
|
-
let src = noise(stride * sh);
|
|
552
|
-
let fast = to_i420(&src, sw, sh, stride, format, dw, dh).unwrap();
|
|
553
|
-
assert!(
|
|
554
|
-
fast.data == reference(&src, sw, sh, stride, format, dw & !1, dh & !1),
|
|
555
|
-
"{format:?} {sw}x{sh} -> {dw}x{dh}"
|
|
556
|
-
);
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
#[test]
|
|
562
|
-
fn padded_bgra_rows_remain_aligned_in_stills_and_video() {
|
|
563
|
-
// ScreenCaptureKit pads 3420 BGRA pixels from 13680 to 13696 bytes/row.
|
|
564
|
-
let src = [
|
|
565
|
-
0, 0, 255, 255, 0, 0, 255, 255, 0, 255, 0, 0, // red row + green padding
|
|
566
|
-
255, 0, 0, 255, 255, 0, 0, 255, 0, 255, 0, 0, // blue row + green padding
|
|
567
|
-
];
|
|
568
|
-
assert_eq!(
|
|
569
|
-
to_bgrx(&src, 2, 2, 12, PixelFormat::Bgra, 2, 2).unwrap(),
|
|
570
|
-
[src[0..8].to_vec(), src[12..20].to_vec()].concat()
|
|
571
|
-
);
|
|
572
|
-
let video = to_i420(&src, 2, 2, 12, PixelFormat::Bgra, 2, 2).unwrap();
|
|
573
|
-
assert_eq!(video.y_plane(), &[82, 82, 41, 41]);
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
#[test]
|
|
577
|
-
fn unsupported_format_is_reported_not_guessed() {
|
|
578
|
-
assert!(to_i420(&vec![0u8; 64], 4, 4, 16, PixelFormat::Unsupported, 2, 2).is_none());
|
|
579
|
-
}
|
|
580
|
-
}
|
package/engine/src/cursor.rs
CHANGED
|
@@ -32,26 +32,3 @@ impl Reporter {
|
|
|
32
32
|
});
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
-
|
|
36
|
-
#[cfg(test)]
|
|
37
|
-
mod tests {
|
|
38
|
-
use super::*;
|
|
39
|
-
#[test]
|
|
40
|
-
fn reports_initial_motion_and_visibility_but_not_unchanged_polls() {
|
|
41
|
-
let (tx, rx) = std::sync::mpsc::channel();
|
|
42
|
-
let mut reporter = Reporter::new(Box::new(move |p| {
|
|
43
|
-
tx.send(p).unwrap();
|
|
44
|
-
}));
|
|
45
|
-
reporter.update(10, 20, true);
|
|
46
|
-
reporter.update(10, 20, true);
|
|
47
|
-
reporter.update(30, 40, true);
|
|
48
|
-
reporter.update(30, 40, false);
|
|
49
|
-
let notices: Vec<_> = rx.try_iter().collect();
|
|
50
|
-
assert_eq!(notices.len(), 3);
|
|
51
|
-
assert_eq!((notices[1].x, notices[1].y), (30, 40));
|
|
52
|
-
assert!(!notices[2].visible);
|
|
53
|
-
assert!(notices
|
|
54
|
-
.windows(2)
|
|
55
|
-
.all(|p| p[0].timestamp_us <= p[1].timestamp_us));
|
|
56
|
-
}
|
|
57
|
-
}
|
package/engine/src/input.rs
CHANGED
|
@@ -300,68 +300,3 @@ impl Drop for InputDevices {
|
|
|
300
300
|
}
|
|
301
301
|
}
|
|
302
302
|
}
|
|
303
|
-
|
|
304
|
-
#[cfg(test)]
|
|
305
|
-
mod tests {
|
|
306
|
-
use super::*;
|
|
307
|
-
|
|
308
|
-
#[test]
|
|
309
|
-
fn button_numbers_map_to_the_expected_pointer_buttons() {
|
|
310
|
-
assert_eq!(Button::from_number(1), Button::Left);
|
|
311
|
-
assert_eq!(Button::from_number(2), Button::Middle);
|
|
312
|
-
assert_eq!(Button::from_number(3), Button::Right);
|
|
313
|
-
assert_eq!(Button::from_number(9), Button::Left);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
#[test]
|
|
317
|
-
fn held_state_releases_exactly_what_this_session_pressed() {
|
|
318
|
-
let mut held = HeldState::default();
|
|
319
|
-
held.key(29, true); // left ctrl
|
|
320
|
-
held.key(46, true); // c
|
|
321
|
-
held.button(Button::Left, true);
|
|
322
|
-
// A repeat press is the same press, not a second one, so the plan still
|
|
323
|
-
// releases it exactly once.
|
|
324
|
-
held.key(29, true);
|
|
325
|
-
|
|
326
|
-
let (buttons, keys) = held.release_plan();
|
|
327
|
-
assert_eq!(buttons, vec![Button::Left]);
|
|
328
|
-
assert_eq!(keys, vec![29, 46]);
|
|
329
|
-
assert!(
|
|
330
|
-
held.release_plan().1.is_empty(),
|
|
331
|
-
"releasing twice releases nothing"
|
|
332
|
-
);
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
#[test]
|
|
336
|
-
fn an_explicit_release_leaves_only_what_is_still_pressed() {
|
|
337
|
-
let mut held = HeldState::default();
|
|
338
|
-
held.key(29, true);
|
|
339
|
-
held.key(46, true);
|
|
340
|
-
held.key(46, false);
|
|
341
|
-
let (buttons, keys) = held.release_plan();
|
|
342
|
-
assert!(buttons.is_empty());
|
|
343
|
-
assert_eq!(keys, vec![29]);
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
/// Proving the backend is really available here is the point: if this host
|
|
347
|
-
/// cannot create the devices, the engine must say so rather than offer a
|
|
348
|
-
/// control surface that does nothing.
|
|
349
|
-
#[test]
|
|
350
|
-
fn this_host_can_create_and_destroy_the_virtual_devices() {
|
|
351
|
-
// This touches the real desktop when uinput is writable. Ordinary
|
|
352
|
-
// suites must never create devices on the owner's active session.
|
|
353
|
-
if std::env::var("DESKLINK_TEST_UINPUT").as_deref() != Ok("1") {
|
|
354
|
-
return;
|
|
355
|
-
}
|
|
356
|
-
match InputDevices::create(2560, 1440) {
|
|
357
|
-
Ok(_devices) => {}
|
|
358
|
-
Err(error) => {
|
|
359
|
-
let unavailable = probe().unwrap_err();
|
|
360
|
-
assert!(
|
|
361
|
-
error.to_string().contains(&unavailable.reason),
|
|
362
|
-
"a creation failure must be the reported availability problem"
|
|
363
|
-
);
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
}
|
package/engine/src/input_mac.rs
CHANGED
|
@@ -299,14 +299,3 @@ impl InputDevices {
|
|
|
299
299
|
}
|
|
300
300
|
}
|
|
301
301
|
}
|
|
302
|
-
|
|
303
|
-
#[cfg(test)]
|
|
304
|
-
mod tests {
|
|
305
|
-
use super::*;
|
|
306
|
-
|
|
307
|
-
#[test]
|
|
308
|
-
fn command_key_uses_quartz_command_flag() {
|
|
309
|
-
assert_eq!(modifier_flag(keycode::LEFT_META), 0x100000);
|
|
310
|
-
assert_eq!(modifier_flag(0), 0);
|
|
311
|
-
}
|
|
312
|
-
}
|
package/engine/src/keeper.rs
CHANGED
|
@@ -483,140 +483,3 @@ fn fatal(out: &mut impl Write, error: &str) -> Result<()> {
|
|
|
483
483
|
write_line(out, &serde_json::json!({ "error": error }).to_string())?;
|
|
484
484
|
Err(anyhow!("{error}"))
|
|
485
485
|
}
|
|
486
|
-
|
|
487
|
-
#[cfg(test)]
|
|
488
|
-
mod tests {
|
|
489
|
-
use super::*;
|
|
490
|
-
|
|
491
|
-
#[test]
|
|
492
|
-
fn content_fills_the_screen_and_helpers_are_parked() {
|
|
493
|
-
assert_eq!(kiosk_geometry(1240, 760, 1280, 800), (0, 0, 1280, 800));
|
|
494
|
-
// Exactly at the limit is content; one pixel under is not.
|
|
495
|
-
assert_eq!(kiosk_geometry(120, 120, 1280, 800), (0, 0, 1280, 800));
|
|
496
|
-
assert_eq!(
|
|
497
|
-
kiosk_geometry(119, 800, 1280, 800),
|
|
498
|
-
(i32::from(PARKED_XY), i32::from(PARKED_XY), 119, 800)
|
|
499
|
-
);
|
|
500
|
-
assert_eq!(
|
|
501
|
-
kiosk_geometry(1000, 24, 1280, 800),
|
|
502
|
-
(i32::from(PARKED_XY), i32::from(PARKED_XY), 1000, 24)
|
|
503
|
-
);
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
#[test]
|
|
507
|
-
fn wm_class_reads_the_class_not_the_instance() {
|
|
508
|
-
assert_eq!(
|
|
509
|
-
decode_wm_class(b"google-chrome\0Google-chrome\0"),
|
|
510
|
-
Some(String::from("Google-chrome"))
|
|
511
|
-
);
|
|
512
|
-
assert_eq!(decode_wm_class(b"instance-only\0"), None);
|
|
513
|
-
assert_eq!(decode_wm_class(b""), None);
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
#[test]
|
|
517
|
-
fn titles_come_from_either_name_property_and_blank_is_none() {
|
|
518
|
-
assert_eq!(
|
|
519
|
-
decode_title("Pricing — Acme Store"),
|
|
520
|
-
Some(String::from("Pricing — Acme Store"))
|
|
521
|
-
);
|
|
522
|
-
assert_eq!(
|
|
523
|
-
decode_title("PPF probe - Google Chrome"),
|
|
524
|
-
Some(String::from("PPF probe - Google Chrome"))
|
|
525
|
-
);
|
|
526
|
-
assert_eq!(decode_title(" "), None);
|
|
527
|
-
assert_eq!(decode_title(""), None);
|
|
528
|
-
assert_eq!(
|
|
529
|
-
decode_title(&latin1(b"Gr\xf6\xdfe")),
|
|
530
|
-
Some(String::from("Gr\u{00f6}\u{00df}e"))
|
|
531
|
-
);
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
#[test]
|
|
535
|
-
fn the_report_line_is_exactly_the_promised_shape() {
|
|
536
|
-
let line = window_line(&[KeeperWindow {
|
|
537
|
-
id: 0x0040_0001,
|
|
538
|
-
title: Some(String::from("PPF probe - Google Chrome")),
|
|
539
|
-
class: Some(String::from("Google-chrome")),
|
|
540
|
-
pid: Some(1003571),
|
|
541
|
-
width: 1280,
|
|
542
|
-
height: 800,
|
|
543
|
-
}]);
|
|
544
|
-
let parsed: serde_json::Value = serde_json::from_str(&line).unwrap();
|
|
545
|
-
assert_eq!(
|
|
546
|
-
parsed,
|
|
547
|
-
serde_json::json!({
|
|
548
|
-
"windows": [{
|
|
549
|
-
"id": 0x40_0001_u64,
|
|
550
|
-
"title": "PPF probe - Google Chrome",
|
|
551
|
-
"class": "Google-chrome",
|
|
552
|
-
"pid": 1003571,
|
|
553
|
-
"width": 1280,
|
|
554
|
-
"height": 800,
|
|
555
|
-
}]
|
|
556
|
-
})
|
|
557
|
-
);
|
|
558
|
-
assert_eq!(window_line(&[]), r#"{"windows":[]}"#);
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
#[test]
|
|
562
|
-
fn missing_fields_are_null_not_absent() {
|
|
563
|
-
let line = window_line(&[KeeperWindow {
|
|
564
|
-
id: 7,
|
|
565
|
-
title: None,
|
|
566
|
-
class: None,
|
|
567
|
-
pid: None,
|
|
568
|
-
width: 100,
|
|
569
|
-
height: 100,
|
|
570
|
-
}]);
|
|
571
|
-
let parsed: serde_json::Value = serde_json::from_str(&line).unwrap();
|
|
572
|
-
let window = &parsed["windows"][0];
|
|
573
|
-
assert_eq!(window["title"], serde_json::Value::Null);
|
|
574
|
-
assert_eq!(window["class"], serde_json::Value::Null);
|
|
575
|
-
assert_eq!(window["pid"], serde_json::Value::Null);
|
|
576
|
-
assert!(window.get("width").is_some());
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
#[test]
|
|
580
|
-
fn an_unopenable_display_is_refused_in_one_error_line() {
|
|
581
|
-
let mut out = Vec::new();
|
|
582
|
-
assert!(keep(Some("not a display"), &mut out).is_err());
|
|
583
|
-
let line: serde_json::Value =
|
|
584
|
-
serde_json::from_str(&String::from_utf8(out).unwrap()).unwrap();
|
|
585
|
-
assert!(line["error"]
|
|
586
|
-
.as_str()
|
|
587
|
-
.is_some_and(|error| !error.is_empty()));
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
#[test]
|
|
591
|
-
fn a_refused_keeper_says_why_in_its_error_line() {
|
|
592
|
-
let mut out = Vec::new();
|
|
593
|
-
let error = fatal(&mut out, "another window manager").unwrap_err();
|
|
594
|
-
assert_eq!(error.to_string(), "another window manager");
|
|
595
|
-
assert_eq!(
|
|
596
|
-
String::from_utf8(out).unwrap(),
|
|
597
|
-
"{\"error\":\"another window manager\"}\n"
|
|
598
|
-
);
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
#[test]
|
|
602
|
-
fn the_display_flag_has_one_spelling() {
|
|
603
|
-
let flag = |args: &[&str]| {
|
|
604
|
-
display_argument(
|
|
605
|
-
&args
|
|
606
|
-
.iter()
|
|
607
|
-
.map(|arg| String::from(*arg))
|
|
608
|
-
.collect::<Vec<_>>(),
|
|
609
|
-
)
|
|
610
|
-
};
|
|
611
|
-
assert_eq!(flag(&["--display", ":42"]), Some(String::from(":42")));
|
|
612
|
-
assert_eq!(
|
|
613
|
-
flag(&["--display", ":42", "--x"]),
|
|
614
|
-
Some(String::from(":42"))
|
|
615
|
-
);
|
|
616
|
-
assert_eq!(flag(&[]), None);
|
|
617
|
-
assert_eq!(flag(&["--display"]), None);
|
|
618
|
-
// The undocumented `--display=:42` spelling is not a display: `keep`
|
|
619
|
-
// refuses it rather than guessing what was meant.
|
|
620
|
-
assert_eq!(flag(&["--display=:42"]), None);
|
|
621
|
-
}
|
|
622
|
-
}
|