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/sunshine.rs12.2 KB
1
use std::path::PathBuf;2
use std::process::{Command, Stdio};3
use std::sync::mpsc::{self, Sender};4
use std::thread::{self, JoinHandle};5
6
use serde::Deserialize;7
8
use crate::diagnostics::{self, Level};9
10
pub const FIRST_BASE_PORT: u16 = 54_321;11
pub const PORT_STRIDE: u16 = 27;12
const LAST_FAMILY_OFFSET: u16 = 21;13
14
#[derive(Clone, Copy, Debug, PartialEq, Eq)]15
pub(crate) enum CaptureBackend {16
Auto,17
Wgc,18
}19
20
impl CaptureBackend {21
pub(crate) fn as_str(self) -> &'static str {22
match self {23
Self::Auto => "auto",24
Self::Wgc => "wgc",25
}26
}27
}28
29
#[derive(Clone, Debug)]30
pub struct DeploymentEvent {31
pub generation: u64,32
pub group_id: u32,33
pub display_id: String,34
pub requested_port: u16,35
pub port: Option<u16>,36
pub error: Option<String>,37
}38
39
enum SupervisorCommand {40
Deploy {41
generation: u64,42
group_id: u32,43
display_id: String,44
requested_port: u16,45
capture: CaptureBackend,46
},47
StopAll,48
Shutdown,49
}50
51
#[derive(Clone)]52
pub struct SupervisorSender(Sender<SupervisorCommand>);53
54
pub struct Supervisor {55
sender: SupervisorSender,56
worker: Option<JoinHandle<()>>,57
}58
59
impl SupervisorSender {60
pub fn start(61
&self,62
generation: u64,63
group_id: u32,64
display_id: String,65
requested_port: u16,66
capture: CaptureBackend,67
) {68
let _ = self.0.send(SupervisorCommand::Deploy {69
generation,70
group_id,71
display_id,72
requested_port,73
capture,74
});75
}76
77
pub fn stop_all(&self) {78
let _ = self.0.send(SupervisorCommand::StopAll);79
}80
}81
82
impl Supervisor {83
pub fn spawn(emit: impl Fn(DeploymentEvent) + Send + 'static) -> Self {84
let (tx, rx) = mpsc::channel();85
let sender = SupervisorSender(tx);86
let worker = thread::spawn(move || {87
while let Ok(command) = rx.recv() {88
match command {89
SupervisorCommand::Deploy {90
generation,91
group_id,92
display_id,93
requested_port,94
capture,95
} => {96
let result = invoke_deploy(group_id, &display_id, requested_port, capture);97
let event = match result {98
Ok(result) => {99
diagnostics::log(100
Level::Info,101
"sunshine",102
"instance",103
None,104
format!(105
"started group {} on port {} (pid {}, capture={})",106
group_id,107
result.port,108
result.pid,109
capture.as_str()110
),111
);112
DeploymentEvent {113
generation,114
group_id,115
display_id,116
requested_port,117
port: Some(result.port),118
error: None,119
}120
}121
Err(error) => {122
diagnostics::log(123
Level::Error,124
"sunshine",125
"instance",126
None,127
format!(128
"starting group {} with capture={} failed: {error}",129
group_id,130
capture.as_str()131
),132
);133
DeploymentEvent {134
generation,135
group_id,136
display_id,137
requested_port,138
port: None,139
error: Some(error),140
}141
}142
};143
emit(event);144
}145
SupervisorCommand::StopAll => {146
if let Err(error) = invoke_stop_all() {147
diagnostics::log(Level::Warn, "sunshine", "stop-all", None, error);148
}149
}150
SupervisorCommand::Shutdown => {151
if let Err(error) = invoke_stop_all() {152
diagnostics::log(Level::Warn, "sunshine", "shutdown", None, error);153
}154
break;155
}156
}157
}158
});159
Self {160
sender,161
worker: Some(worker),162
}163
}164
165
pub fn sender(&self) -> SupervisorSender {166
self.sender.clone()167
}168
169
pub fn shutdown(mut self) {170
let _ = self.sender.0.send(SupervisorCommand::Shutdown);171
if let Some(worker) = self.worker.take() {172
let _ = worker.join();173
}174
}175
}176
177
pub fn default_port_for_slot(slot: usize) -> Result<u16, String> {178
let slot = u16::try_from(slot)179
.map_err(|_| "Too many Sunshine instances for the available port range".to_string())?;180
let offset = slot181
.checked_mul(PORT_STRIDE)182
.ok_or_else(|| "Sunshine port allocation overflowed".to_string())?;183
let port = FIRST_BASE_PORT184
.checked_add(offset)185
.ok_or_else(|| "Sunshine port allocation overflowed".to_string())?;186
if port.checked_add(LAST_FAMILY_OFFSET).is_none() {187
return Err("No complete Sunshine port family remains".into());188
}189
Ok(port)190
}191
192
#[derive(Deserialize)]193
#[serde(rename_all = "camelCase")]194
struct ScriptResponse {195
ok: bool,196
#[serde(default)]197
port: Option<u16>,198
#[serde(default)]199
pid: Option<u32>,200
#[serde(default)]201
message: Option<String>,202
}203
204
struct DeployResult {205
port: u16,206
pid: u32,207
}208
209
fn invoke_deploy(210
group_id: u32,211
display_id: &str,212
requested_port: u16,213
capture: CaptureBackend,214
) -> Result<DeployResult, String> {215
let script = manager_script_path()?;216
let output = deploy_command(&script, group_id, display_id, requested_port, capture)217
.output()218
.map_err(|error| format!("Couldn’t launch the Sunshine manager: {error}"))?;219
let response = parse_response(&output.stdout, &output.stderr)?;220
if !output.status.success() || !response.ok {221
return Err(response.message.unwrap_or_else(|| {222
format!(223
"Sunshine manager exited with status {}",224
output.status.code().unwrap_or(-1)225
)226
}));227
}228
let port = response229
.port230
.ok_or_else(|| "Sunshine manager did not report its deployed port".to_string())?;231
let pid = response232
.pid233
.ok_or_else(|| "Sunshine manager did not report its process ID".to_string())?;234
Ok(DeployResult { port, pid })235
}236
237
fn deploy_command(238
script: &PathBuf,239
group_id: u32,240
display_id: &str,241
requested_port: u16,242
capture: CaptureBackend,243
) -> Command {244
let mut command = powershell_command(script);245
command246
.arg("-Action")247
.arg("Start")248
.arg("-GroupId")249
.arg(group_id.to_string())250
.arg("-DisplayId")251
.arg(display_id)252
.arg("-Port")253
.arg(requested_port.to_string())254
.arg("-Capture")255
.arg(capture.as_str());256
command257
}258
259
fn invoke_stop_all() -> Result<(), String> {260
let script = manager_script_path()?;261
let output = powershell_command(&script)262
.arg("-Action")263
.arg("StopAll")264
.output()265
.map_err(|error| format!("Couldn’t launch Sunshine cleanup: {error}"))?;266
let response = parse_response(&output.stdout, &output.stderr)?;267
if output.status.success() && response.ok {268
Ok(())269
} else {270
Err(response.message.unwrap_or_else(|| {271
format!(272
"Sunshine cleanup exited with status {}",273
output.status.code().unwrap_or(-1)274
)275
}))276
}277
}278
279
fn powershell_command(script: &PathBuf) -> Command {280
let mut command = Command::new("powershell.exe");281
command282
.arg("-NoLogo")283
.arg("-NoProfile")284
.arg("-NonInteractive")285
.arg("-ExecutionPolicy")286
.arg("Bypass")287
.arg("-File")288
.arg(script)289
.stdin(Stdio::null())290
.stdout(Stdio::piped())291
.stderr(Stdio::piped());292
#[cfg(windows)]293
{294
use std::os::windows::process::CommandExt;295
command.creation_flags(0x0800_0000);296
}297
command298
}299
300
fn parse_response(stdout: &[u8], stderr: &[u8]) -> Result<ScriptResponse, String> {301
let stdout = String::from_utf8_lossy(stdout);302
if let Some(line) = stdout.lines().rev().find(|line| !line.trim().is_empty()) {303
return serde_json::from_str(line).map_err(|error| {304
format!(305
"Sunshine manager returned invalid JSON ({error}): {}",306
line.trim()307
)308
});309
}310
let stderr = String::from_utf8_lossy(stderr);311
let detail = stderr.trim();312
if detail.is_empty() {313
Err("Sunshine manager returned no result".into())314
} else {315
Err(format!("Sunshine manager returned no result: {detail}"))316
}317
}318
319
fn manager_script_path() -> Result<PathBuf, String> {320
let installed = std::env::current_exe()321
.ok()322
.and_then(|path| path.parent().map(|parent| parent.join("installer")))323
.map(|path| path.join("manage-sunshine-instance.ps1"));324
let development =325
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("installer/manage-sunshine-instance.ps1");326
installed327
.filter(|path| path.is_file())328
.or_else(|| development.is_file().then_some(development))329
.ok_or_else(|| "The Sunshine instance manager is missing from this installation".into())330
}331
332
#[cfg(test)]333
mod tests {334
use super::*;335
336
#[test]337
fn default_ports_leave_room_for_the_complete_sunshine_port_family() {338
assert_eq!(default_port_for_slot(0).unwrap(), 54_321);339
assert_eq!(default_port_for_slot(1).unwrap(), 54_348);340
assert_eq!(default_port_for_slot(2).unwrap(), 54_375);341
}342
343
#[test]344
fn default_port_allocation_rejects_ranges_past_u16() {345
assert!(default_port_for_slot(1_000).is_err());346
assert!(default_port_for_slot(usize::MAX).is_err());347
}348
349
#[test]350
fn parses_the_last_nonempty_json_line() {351
let response = parse_response(352
b"diagnostic line\n{\"ok\":true,\"port\":54321,\"pid\":42}\n",353
b"",354
)355
.unwrap();356
assert!(response.ok);357
assert_eq!(response.port, Some(54_321));358
assert_eq!(response.pid, Some(42));359
}360
361
#[test]362
fn deploy_command_passes_capture_as_a_separate_manager_argument() {363
for (capture, expected_capture) in364
[(CaptureBackend::Auto, "auto"), (CaptureBackend::Wgc, "wgc")]365
{366
let command = deploy_command(367
&PathBuf::from(r"C:\Program Files\SBMS\installer\manage-sunshine-instance.ps1"),368
2,369
"{01234567-89ab-cdef-0123-456789abcdef}",370
54_375,371
capture,372
);373
let arguments: Vec<_> = command374
.get_args()375
.map(|argument| argument.to_string_lossy().into_owned())376
.collect();377
378
assert_eq!(379
&arguments[arguments.len() - 10..],380
[381
"-Action",382
"Start",383
"-GroupId",384
"2",385
"-DisplayId",386
"{01234567-89ab-cdef-0123-456789abcdef}",387
"-Port",388
"54375",389
"-Capture",390
expected_capture,391
]392
);393
}394
}395
}396