EvanZhu0721 / 公开仓库
SBMS
SBMS complete the calculation rules for the default logical desktop topology relationship and the physical dimensions of the displays in the Windows multi-screen collaboration system.
文件
../ bininputuiconfig.rs81.2 KBcontrol.rs4.1 KBcontroller.rs43.6 KBdiagnostics.rs24.2 KBdisplay.rs29.8 KBgeometry.rs21.4 KBgpu_renderer.rs37.4 KBinput.rs79.7 KBlaunch_broker.rs10.3 KBlib.rs350 Blimits.rs714 Bmain.rs24.4 KBmapping.rs46.5 KBnetwork.rs9.2 KBrenderer.rs20.6 KBsession_gate.rs14.9 KBsunshine.rs12.2 KBui.rs68.5 KBvirtual_display.rs7.2 KBwin32_flyout.rs3.4 KBwin32_tray.rs22.3 KBwindow_migration.rs43.8 KBsrc/main.rs24.4 KB
1
use std::error::Error;2
use std::fmt::{Display, Formatter};3
use std::fs;4
use std::io;5
use std::path::Path;6
use std::sync::Arc;7
use std::time::Duration;8
9
use sbms::config::{ConfigError, ConfigProfileStore, ConfigStore, GroupRouteConfig};10
use sbms::diagnostics::{self, Level, MappingSessionId};11
use sbms::display::{active_displays, unique_physical_display};12
use sbms::geometry::Rotation;13
use sbms::launch_broker::{self, LaunchDisposition};14
use sbms::mapping::{15
MappingError, MappingEvent, MappingGroupRequest, MappingPlan, MappingRoute, MappingSession,16
};17
use sbms::renderer::RendererEvent;18
use sbms::session_gate::VirtualMode;19
use windows::Win32::UI::HiDpi::{20
DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2, SetProcessDpiAwarenessContext,21
};22
23
#[derive(Debug)]24
struct MapPreflightError {25
stage: &'static str,26
message: String,27
}28
29
impl MapPreflightError {30
fn new(stage: &'static str, error: impl Display) -> Self {31
Self {32
stage,33
message: error.to_string(),34
}35
}36
}37
38
impl Display for MapPreflightError {39
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {40
write!(formatter, "{}: {}", self.stage, self.message)41
}42
}43
44
impl Error for MapPreflightError {}45
46
fn main() -> Result<(), Box<dyn Error>> {47
if let Err(error) = diagnostics::init() {48
eprintln!("warning: diagnostics initialization failed: {error}");49
}50
diagnostics::log(Level::Info, "cli", "startup", None, "SBMS CLI started");51
let result = run();52
if let Err(error) = &result {53
let message = error.to_string();54
if !diagnostics::latest_error_matches(&message) {55
diagnostics::log(Level::Error, "cli", "command", None, message);56
}57
}58
result59
}60
61
fn run() -> Result<(), Box<dyn Error>> {62
unsafe { SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) }?;63
let mut arguments = std::env::args().skip(1);64
match arguments.next().as_deref() {65
Some("--version") => version(arguments),66
Some("list") => list(arguments),67
Some("map") => map(arguments),68
Some("plan") => plan(arguments),69
Some("config") => config(arguments),70
Some("shutdown") => shutdown(arguments),71
Some("ui") if arguments.next().is_none() => match launch_broker::route_tray(true)? {72
LaunchDisposition::Exit => Ok(()),73
LaunchDisposition::RunHere(instance) => sbms::ui::run_host(instance, true),74
},75
_ => usage(),76
}77
}78
79
fn plan(mut arguments: impl Iterator<Item = String>) -> Result<(), Box<dyn Error>> {80
match arguments.next().as_deref() {81
Some("validate") => {82
let path = arguments83
.next()84
.ok_or("plan validate requires a JSON file")?;85
if arguments.next().is_some() {86
usage();87
}88
let plan = load_plan(&path)?;89
plan.validate()?;90
println!("valid_groups={}", plan.groups.len());91
Ok(())92
}93
Some("run") => {94
let path = arguments.next().ok_or("plan run requires a JSON file")?;95
let mut hold = None;96
while let Some(argument) = arguments.next() {97
match argument.as_str() {98
"--hold-ms" if hold.is_none() => {99
let milliseconds = arguments100
.next()101
.ok_or("--hold-ms requires a value")?102
.parse::<u64>()?;103
hold = Some(Duration::from_millis(milliseconds));104
}105
_ => usage(),106
}107
}108
let plan = load_plan(&path)?;109
let session_id = diagnostics::new_mapping_session_id();110
diagnostics::log(111
Level::Info,112
"mapping",113
"start",114
Some(session_id),115
format!("starting mapping plan with {} group(s)", plan.groups.len()),116
);117
let reporter = Arc::new(move |event| match event {118
MappingEvent::GroupReady(group) => println!(119
"group_ready={} route={} device={} sunshine={} source={}",120
group.id,121
route_name(&group.route),122
group.source_device_name,123
group.sunshine_id.as_deref().unwrap_or("-"),124
group.source_id125
),126
MappingEvent::Renderer {127
id,128
event: RendererEvent::Fps(fps),129
} => println!("group={id} fps={fps}"),130
MappingEvent::Renderer {131
id,132
event: RendererEvent::Failed(error),133
} => {134
diagnostics::log(135
Level::Error,136
"renderer",137
"renderer",138
Some(session_id),139
format!("group={id}: {error}"),140
);141
eprintln!("group={id} renderer_error={error}");142
}143
MappingEvent::Renderer {144
id,145
event: RendererEvent::TopologyLost,146
} => {147
diagnostics::log(148
Level::Warn,149
"renderer",150
"topology-lost",151
Some(session_id),152
format!("group={id}: desktop duplication topology was lost"),153
);154
eprintln!("group={id} renderer_topology_lost");155
}156
MappingEvent::Topology { message } => {157
diagnostics::log(158
Level::Debug,159
"mapping",160
"topology",161
Some(session_id),162
message,163
);164
}165
});166
let mut session = MappingSession::start_plan_with_reporter(plan, reporter)167
.map_err(|error| log_mapping_error(session_id, error))?;168
diagnostics::log(169
Level::Info,170
"mapping",171
"running",172
Some(session_id),173
format!("mapping active with {} group(s)", session.groups().len()),174
);175
println!("running_groups={}", session.groups().len());176
wait(hold)?;177
session178
.stop()179
.map_err(|error| log_mapping_error(session_id, error))?;180
diagnostics::log(181
Level::Info,182
"mapping",183
"stop",184
Some(session_id),185
"mapping stopped",186
);187
println!("stopped");188
Ok(())189
}190
_ => usage(),191
}192
}193
194
fn load_plan(path: &str) -> Result<MappingPlan, Box<dyn Error>> {195
let bytes = std::fs::read(path)?;196
Ok(serde_json::from_slice(&bytes)?)197
}198
199
fn route_name(route: &MappingRoute) -> &'static str {200
match route {201
MappingRoute::Mirror { .. } => "mirror",202
MappingRoute::StreamOnly => "stream_only",203
}204
}205
206
fn shutdown(mut arguments: impl Iterator<Item = String>) -> Result<(), Box<dyn Error>> {207
if arguments.next().is_some() {208
usage();209
}210
println!(211
"{}",212
if sbms::control::signal_shutdown()? {213
"shutdown=signaled"214
} else {215
"shutdown=not-running"216
}217
);218
Ok(())219
}220
221
fn version(mut arguments: impl Iterator<Item = String>) -> Result<(), Box<dyn Error>> {222
if arguments.next().is_some() {223
usage();224
}225
println!("sbms {}", env!("CARGO_PKG_VERSION"));226
Ok(())227
}228
229
fn list(mut arguments: impl Iterator<Item = String>) -> Result<(), Box<dyn Error>> {230
if arguments.next().is_some() {231
usage();232
}233
for display in active_displays()? {234
println!(235
"id={}\tname={}\tdevice={}\tconnector={}\tsunshine={}\trect={},{},{},{}\tnative={}x{}\trefresh={}/{}\t{}{}",236
display.id,237
display.name,238
display.device_name,239
display.connector_index,240
display.sunshine_id.as_deref().unwrap_or("-"),241
display.rect.left,242
display.rect.top,243
display.rect.right - display.rect.left,244
display.rect.bottom - display.rect.top,245
display.native_width,246
display.native_height,247
display.refresh_numerator,248
display.refresh_denominator,249
if display.primary { "primary " } else { "" },250
if display.virtual_display {251
"virtual"252
} else {253
"physical"254
}255
);256
}257
Ok(())258
}259
260
fn map(mut arguments: impl Iterator<Item = String>) -> Result<(), Box<dyn Error>> {261
let mut target = None;262
let mut hold = None;263
while let Some(argument) = arguments.next() {264
match argument.as_str() {265
"--target" if target.is_none() => {266
target = Some(arguments.next().ok_or("--target requires a display id")?);267
}268
"--hold-ms" if hold.is_none() => {269
let milliseconds = arguments270
.next()271
.ok_or("--hold-ms requires a value")?272
.parse::<u64>()?;273
hold = Some(Duration::from_millis(milliseconds));274
}275
_ => usage(),276
}277
}278
let session_id = diagnostics::new_mapping_session_id();279
diagnostics::log(280
Level::Info,281
"mapping",282
"start",283
Some(session_id),284
"starting mapping",285
);286
let plan =287
configured_map_plan(target).map_err(|error| log_map_preflight_error(session_id, error))?;288
let reporter = Arc::new(move |event| match event {289
MappingEvent::Renderer {290
id: 0,291
event: RendererEvent::Fps(fps),292
} => println!("fps={fps}"),293
MappingEvent::Renderer {294
id: 0,295
event: RendererEvent::Failed(error),296
} => {297
diagnostics::log(298
Level::Error,299
"renderer",300
"renderer",301
Some(session_id),302
&error,303
);304
eprintln!("renderer_error={error}");305
}306
MappingEvent::Renderer {307
id: 0,308
event: RendererEvent::TopologyLost,309
} => {310
diagnostics::log(311
Level::Warn,312
"renderer",313
"topology-lost",314
Some(session_id),315
"desktop duplication topology was lost",316
);317
eprintln!("renderer_topology_lost");318
}319
_ => {}320
});321
let mut session = MappingSession::start_plan_with_reporter(plan, reporter)322
.map_err(|error| log_mapping_error(session_id, error))?;323
let source_id = session324
.groups()325
.next()326
.expect("a running mapping session has at least one group")327
.source_id328
.clone();329
diagnostics::log(330
Level::Info,331
"mapping",332
"running",333
Some(session_id),334
format!("mapping active on {source_id}"),335
);336
println!("running={source_id}");337
wait(hold)?;338
session339
.stop()340
.map_err(|error| log_mapping_error(session_id, error))?;341
diagnostics::log(342
Level::Info,343
"mapping",344
"stop",345
Some(session_id),346
"mapping stopped",347
);348
println!("stopped");349
Ok(())350
}351
352
fn configured_map_plan(target: Option<String>) -> Result<MappingPlan, MapPreflightError> {353
let snapshot = ConfigProfileStore::default_store()354
.map_err(|error| MapPreflightError::new("config", error))?355
.load_active()356
.map_err(|error| MapPreflightError::new("config", error))?;357
let target = match target {358
Some(target) => target,359
None => {360
let output_one = snapshot.config.group(0).ok_or_else(|| {361
MapPreflightError::new(362
"config",363
"Output 1 is not present in the saved configuration",364
)365
})?;366
match &output_one.route {367
GroupRouteConfig::Mirror {368
target_id: Some(target),369
} => target.clone(),370
GroupRouteConfig::Mirror { target_id: None } => {371
return Err(MapPreflightError::new(372
"config",373
"Output 1 has no saved target display",374
));375
}376
GroupRouteConfig::StreamOnly { .. } => {377
return Err(MapPreflightError::new(378
"config",379
"Output 1 is stream-only; use `sbms plan run` or provide --target",380
));381
}382
}383
}384
};385
let sizing = snapshot.config.groups.iter().find_map(|group| {386
matches!(387
&group.route,388
GroupRouteConfig::Mirror {389
target_id: Some(configured_target)390
} if configured_target.eq_ignore_ascii_case(&target)391
)392
.then_some(group.sizing)393
.flatten()394
});395
let mode = match sizing {396
Some(sizing) => {397
let result = sizing398
.calculate()399
.map_err(|error| MapPreflightError::new("geometry", error))?;400
let refresh_millihz = match result.preferred_refresh_millihz {401
Some(refresh) => refresh,402
None => target_refresh_millihz(&target)?,403
};404
VirtualMode::from_millihz(405
result.virtual_mode.width,406
result.virtual_mode.height,407
refresh_millihz,408
)409
.map_err(|error| MapPreflightError::new("mode", error))?410
}411
None => VirtualMode::default(),412
};413
MappingPlan::new(vec![MappingGroupRequest {414
id: 0,415
mode,416
rotation: Rotation::Deg0,417
route: MappingRoute::Mirror { target },418
}])419
.map_err(|error| MapPreflightError::new(error.stage(), error.message()))420
}421
422
fn target_refresh_millihz(target_id: &str) -> Result<u32, MapPreflightError> {423
let displays = active_displays().map_err(|error| MapPreflightError::new("target", error))?;424
let target = unique_physical_display(&displays, target_id)425
.map_err(|error| MapPreflightError::new("target", error))?;426
if target.refresh_numerator == 0 || target.refresh_denominator == 0 {427
return Err(MapPreflightError::new(428
"mode",429
"target display reported an invalid refresh rate",430
));431
}432
u64::from(target.refresh_numerator)433
.checked_mul(1_000)434
.and_then(|value| value.checked_div(u64::from(target.refresh_denominator)))435
.and_then(|value| u32::try_from(value).ok())436
.filter(|value| *value > 0)437
.ok_or_else(|| {438
MapPreflightError::new("mode", "target refresh rate is outside the supported range")439
})440
}441
442
fn log_map_preflight_error(443
session_id: MappingSessionId,444
error: MapPreflightError,445
) -> MapPreflightError {446
diagnostics::log(447
Level::Error,448
"mapping",449
error.stage,450
Some(session_id),451
error.to_string(),452
);453
error454
}455
456
fn log_mapping_error(session_id: MappingSessionId, error: MappingError) -> MappingError {457
diagnostics::log(458
Level::Error,459
"mapping",460
error.stage(),461
Some(session_id),462
error.to_string(),463
);464
error465
}466
467
fn config(mut arguments: impl Iterator<Item = String>) -> Result<(), Box<dyn Error>> {468
let store = ConfigProfileStore::default_store()?;469
match arguments.next().as_deref() {470
Some("path") if arguments.next().is_none() => {471
println!("{}", store.path().display());472
}473
Some("list") if arguments.next().is_none() => {474
let profiles = store.list()?;475
let summaries = profiles476
.profiles477
.iter()478
.map(|profile| {479
serde_json::json!({480
"id": profile.id,481
"revision": profile.revision,482
"active": profile.id.eq_ignore_ascii_case(&profiles.active_profile),483
})484
})485
.collect::<Vec<_>>();486
println!("{}", serde_json::to_string_pretty(&summaries)?);487
}488
Some("show") => {489
let profile = arguments.next();490
if arguments.next().is_some() {491
usage();492
}493
let snapshot = match profile {494
Some(profile) => store.load_profile(&profile)?,495
None => store.load_active()?,496
};497
println!("{}", serde_json::to_string_pretty(&snapshot.config)?);498
}499
Some("save") => {500
let profile = arguments501
.next()502
.ok_or("config save requires a profile id")?;503
if arguments.next().is_some() {504
usage();505
}506
let snapshot = store.save_active_as(&profile)?;507
println!(508
"saved={} revision={}",509
snapshot.profile.id, snapshot.profile.revision510
);511
}512
Some("import") => {513
let profile = arguments514
.next()515
.ok_or("config import requires a profile id")?;516
let path = arguments517
.next()518
.ok_or("config import requires a JSON file path")?;519
let mut replace = false;520
let mut activate = false;521
for argument in arguments {522
match argument.as_str() {523
"--replace" => replace = true,524
"--activate" => activate = true,525
_ => usage(),526
}527
}528
let imported = ConfigStore::new(path.into()).load_strict()?;529
let (snapshot, is_active) =530
store.save_profile(&profile, &imported, replace, activate)?;531
let reload = is_active.then(signal_config_reload).transpose()?;532
println!(533
"imported={} revision={}{}",534
snapshot.profile.id,535
snapshot.profile.revision,536
reload537
.map(|signaled| format!(" reload={}", reload_status(signaled)))538
.unwrap_or_default()539
);540
}541
Some("export") => {542
let profile = arguments543
.next()544
.ok_or("config export requires a profile id")?;545
let path = arguments546
.next()547
.ok_or("config export requires a JSON file path")?;548
let force = match arguments.next().as_deref() {549
None => false,550
Some("--force") if arguments.next().is_none() => true,551
_ => usage(),552
};553
let snapshot = store.load_profile(&profile)?;554
let destination = Path::new(&path);555
if destination.exists() {556
if same_existing_path(destination, store.path())? {557
return Err("refusing to export over the config profile store".into());558
}559
if !force {560
return Err(format!(561
"refusing to overwrite {path}; pass --force to replace it"562
)563
.into());564
}565
}566
let export_store = ConfigStore::new(path.clone().into());567
if force {568
export_store.save(&snapshot.config)?;569
} else {570
export_store.save_new(&snapshot.config)?;571
}572
println!("exported={} path={path}", snapshot.profile.id);573
}574
Some("activate") => {575
let profile = arguments576
.next()577
.ok_or("config activate requires a profile id")?;578
if arguments.next().is_some() {579
usage();580
}581
let snapshot = store.activate(&profile)?;582
let signaled = signal_config_reload()?;583
println!(584
"active={} revision={} reload={}",585
snapshot.profile.id,586
snapshot.profile.revision,587
reload_status(signaled)588
);589
}590
Some("delete") => {591
let profile = arguments592
.next()593
.ok_or("config delete requires a profile id")?;594
if arguments.next().is_some() {595
usage();596
}597
store.delete(&profile)?;598
println!("deleted={profile}");599
}600
Some("reload") if arguments.next().is_none() => {601
println!("reload={}", reload_status(signal_config_reload()?));602
}603
Some("set-target") => {604
let target = arguments605
.next()606
.ok_or("config set-target requires a display id")?;607
if arguments.next().is_some() {608
usage();609
}610
validate_physical_target(&target)?;611
let snapshot = store.update_active(|config| {612
let output_one = config.group_mut(0).ok_or_else(|| {613
ConfigError::new("Output 1 is not present in the saved configuration")614
})?;615
output_one.route = GroupRouteConfig::Mirror {616
target_id: Some(target),617
};618
Ok(())619
})?;620
println!(621
"saved={} revision={} reload={}",622
snapshot.profile.id,623
snapshot.profile.revision,624
reload_status(signal_config_reload()?)625
);626
}627
Some("clear-target") if arguments.next().is_none() => {628
let snapshot = store.update_active(|config| {629
let output_one = config.group_mut(0).ok_or_else(|| {630
ConfigError::new("Output 1 is not present in the saved configuration")631
})?;632
output_one.route = GroupRouteConfig::Mirror { target_id: None };633
Ok(())634
})?;635
println!(636
"saved={} revision={} reload={}",637
snapshot.profile.id,638
snapshot.profile.revision,639
reload_status(signal_config_reload()?)640
);641
}642
Some("reset") if arguments.next().is_none() => {643
let snapshot = store.reset_active()?;644
println!(645
"reset={} revision={} reload={}",646
snapshot.profile.id,647
snapshot.profile.revision,648
reload_status(signal_config_reload()?)649
);650
}651
_ => usage(),652
}653
Ok(())654
}655
656
fn signal_config_reload() -> Result<bool, Box<dyn Error>> {657
sbms::control::signal_config_reload().map_err(Into::into)658
}659
660
fn reload_status(signaled: bool) -> &'static str {661
if signaled { "signaled" } else { "not-running" }662
}663
664
fn same_existing_path(left: &Path, right: &Path) -> io::Result<bool> {665
let left = fs::canonicalize(left)?;666
let right = fs::canonicalize(right)?;667
Ok(left668
.to_string_lossy()669
.eq_ignore_ascii_case(&right.to_string_lossy()))670
}671
672
fn validate_physical_target(target_id: &str) -> Result<(), Box<dyn Error>> {673
let matches: Vec<_> = active_displays()?674
.into_iter()675
.filter(|display| display.id.eq_ignore_ascii_case(target_id))676
.collect();677
match matches.as_slice() {678
[target] if !target.virtual_display => Ok(()),679
[_] => Err("the saved target cannot be the SBMS virtual display".into()),680
[] => Err(format!("active physical display id not found: {target_id}").into()),681
_ => Err(format!("display id is ambiguous: {target_id}").into()),682
}683
}684
685
fn wait(hold: Option<Duration>) -> io::Result<()> {686
if let Some(duration) = hold {687
std::thread::sleep(duration);688
} else {689
println!("Press Enter to stop.");690
let mut line = String::new();691
io::stdin().read_line(&mut line)?;692
}693
Ok(())694
}695
696
fn usage() -> ! {697
eprintln!(698
"usage:699
sbms --version700
sbms list701
sbms map [--target <monitor-device-path>] [--hold-ms <milliseconds>]702
sbms plan validate <plan.json>703
sbms plan run <plan.json> [--hold-ms <milliseconds>]704
sbms config path|list|show [<profile>]705
sbms config save <profile>706
sbms config import <profile> <file.json> [--replace] [--activate]707
sbms config export <profile> <file.json> [--force]708
sbms config activate|delete <profile>709
sbms config reload710
sbms config set-target <monitor-device-path>|clear-target|reset711
sbms shutdown712
sbms ui"713
);714
std::process::exit(2);715
}716