Evan's Blog
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.

上游 GitHub
READ-ONLY MIRROR

README、源码与 Release 已保存到本站;源码视图固定到 commit a37d6c4b。

main/ root / src
a37d6c4b· 固定源码快照
src/sunshine.rs12.2 KB
1use std::path::PathBuf;
2use std::process::{Command, Stdio};
3use std::sync::mpsc::{self, Sender};
4use std::thread::{self, JoinHandle};
5
6use serde::Deserialize;
7
8use crate::diagnostics::{self, Level};
9
10pub const FIRST_BASE_PORT: u16 = 54_321;
11pub const PORT_STRIDE: u16 = 27;
12const LAST_FAMILY_OFFSET: u16 = 21;
13
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub(crate) enum CaptureBackend {
16 Auto,
17 Wgc,
18}
19
20impl 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)]
30pub 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
39enum 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)]
52pub struct SupervisorSender(Sender<SupervisorCommand>);
53
54pub struct Supervisor {
55 sender: SupervisorSender,
56 worker: Option<JoinHandle<()>>,
57}
58
59impl 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
82impl 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
177pub 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 = slot
181 .checked_mul(PORT_STRIDE)
182 .ok_or_else(|| "Sunshine port allocation overflowed".to_string())?;
183 let port = FIRST_BASE_PORT
184 .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")]
194struct 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
204struct DeployResult {
205 port: u16,
206 pid: u32,
207}
208
209fn 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 = response
229 .port
230 .ok_or_else(|| "Sunshine manager did not report its deployed port".to_string())?;
231 let pid = response
232 .pid
233 .ok_or_else(|| "Sunshine manager did not report its process ID".to_string())?;
234 Ok(DeployResult { port, pid })
235}
236
237fn 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 command
246 .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 command
257}
258
259fn 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
279fn powershell_command(script: &PathBuf) -> Command {
280 let mut command = Command::new("powershell.exe");
281 command
282 .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 command
298}
299
300fn 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
319fn 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 installed
327 .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)]
333mod 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) in
364 [(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<_> = command
374 .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