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/diagnostics.rs24.2 KB
1
use std::collections::VecDeque;2
use std::ffi::OsString;3
use std::fs::{self, OpenOptions};4
use std::io::{self, BufRead, BufReader, Write};5
use std::path::{Path, PathBuf};6
use std::sync::atomic::{AtomicU64, Ordering};7
use std::sync::{Mutex, OnceLock};8
use std::time::{Duration, SystemTime, UNIX_EPOCH};9
10
use serde::{Deserialize, Serialize};11
use windows::Win32::Foundation::{12
CloseHandle, FILETIME, HANDLE, SYSTEMTIME, WAIT_ABANDONED, WAIT_FAILED, WAIT_OBJECT_0,13
};14
use windows::Win32::System::Threading::{15
CreateMutexW, INFINITE, ReleaseMutex, WaitForSingleObject,16
};17
use windows::Win32::System::Time::{FileTimeToSystemTime, SystemTimeToTzSpecificLocalTimeEx};18
use windows::core::w;19
20
const DEFAULT_MAX_FILE_BYTES: u64 = 1536 * 1024;21
const DEFAULT_MEMORY_RECORDS: usize = 200;22
const ROLLING_FILE_COUNT: usize = 3;23
const AUXILIARY_LOG_RETENTION: Duration = Duration::from_secs(30 * 24 * 60 * 60);24
const WINDOWS_EPOCH_OFFSET_TICKS: u64 = 116_444_736_000_000_000;25
const TICKS_PER_MILLISECOND: u64 = 10_000;26
27
static DIAGNOSTICS: OnceLock<Diagnostics> = OnceLock::new();28
static NEXT_MAPPING_SESSION: AtomicU64 = AtomicU64::new(1);29
30
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]31
#[serde(rename_all = "UPPERCASE")]32
pub enum Level {33
Debug,34
Info,35
Warn,36
Error,37
}38
39
#[derive(Clone, Copy, Debug, Eq, PartialEq)]40
pub struct MappingSessionId {41
process_id: u32,42
sequence: u64,43
}44
45
impl std::fmt::Display for MappingSessionId {46
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {47
write!(formatter, "{}-{}", self.process_id, self.sequence)48
}49
}50
51
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]52
pub struct Record {53
pub time: u64,54
#[serde(default, skip_serializing_if = "String::is_empty")]55
pub local_time: String,56
pub level: Level,57
pub module: String,58
pub stage: String,59
pub mapping_session_id: String,60
pub message: String,61
}62
63
pub fn init() -> io::Result<&'static Path> {64
if let Some(diagnostics) = DIAGNOSTICS.get() {65
return Ok(&diagnostics.path);66
}67
68
let path = default_log_path()?;69
let diagnostics = Diagnostics::open(path, DEFAULT_MAX_FILE_BYTES, DEFAULT_MEMORY_RECORDS)?;70
let _ = DIAGNOSTICS.set(diagnostics);71
Ok(&DIAGNOSTICS72
.get()73
.expect("diagnostics must be initialized")74
.path)75
}76
77
pub fn default_log_path() -> io::Result<PathBuf> {78
let local = std::env::var_os("LOCALAPPDATA")79
.filter(|value| !value.is_empty())80
.ok_or_else(|| {81
io::Error::new(82
io::ErrorKind::NotFound,83
"LOCALAPPDATA is unavailable; cannot locate the SBMS log directory",84
)85
})?;86
Ok(PathBuf::from(local)87
.join("SBMS")88
.join("logs")89
.join("sbms.log"))90
}91
92
pub fn new_mapping_session_id() -> MappingSessionId {93
MappingSessionId {94
process_id: std::process::id(),95
sequence: NEXT_MAPPING_SESSION.fetch_add(1, Ordering::Relaxed),96
}97
}98
99
pub fn log(100
level: Level,101
module: impl Into<String>,102
stage: impl Into<String>,103
mapping_session_id: Option<MappingSessionId>,104
message: impl Into<String>,105
) {106
let time = unix_time_millis();107
let record = Record {108
time,109
local_time: local_time_string(time).unwrap_or_default(),110
level,111
module: module.into(),112
stage: stage.into(),113
mapping_session_id: mapping_session_id114
.map(|id| id.to_string())115
.unwrap_or_else(|| "-".into()),116
message: message.into(),117
};118
119
let result = DIAGNOSTICS120
.get()121
.map(|diagnostics| diagnostics.write(record.clone()));122
if let Some(Err(error)) = result {123
eprintln!("SBMS diagnostics write failed: {error}");124
} else if result.is_none() && level == Level::Error {125
eprintln!(126
"SBMS error [{}:{} session={}]: {}",127
record.module, record.stage, record.mapping_session_id, record.message128
);129
}130
}131
132
pub fn recent() -> Vec<Record> {133
DIAGNOSTICS134
.get()135
.map(Diagnostics::recent)136
.unwrap_or_default()137
}138
139
pub fn latest_error_matches(message: &str) -> bool {140
DIAGNOSTICS141
.get()142
.and_then(|diagnostics| diagnostics.recent().pop())143
.is_some_and(|record| record.level == Level::Error && record.message == message)144
}145
146
pub fn persisted_recent() -> io::Result<Vec<Record>> {147
let path = default_log_path()?;148
let _guard = CrossProcessLogGuard::acquire()?;149
let mut records = VecDeque::with_capacity(DEFAULT_MEMORY_RECORDS);150
for candidate in [rotated_path(&path, 2), rotated_path(&path, 1), path] {151
let file = match fs::File::open(&candidate) {152
Ok(file) => file,153
Err(error) if error.kind() == io::ErrorKind::NotFound => continue,154
Err(error) => return Err(error),155
};156
for line in BufReader::new(file).lines() {157
let line = line?;158
let Ok(record) = serde_json::from_str::<Record>(&line) else {159
continue;160
};161
if records.len() == DEFAULT_MEMORY_RECORDS {162
records.pop_front();163
}164
records.push_back(record);165
}166
}167
Ok(records.into())168
}169
170
fn unix_time_millis() -> u64 {171
SystemTime::now()172
.duration_since(UNIX_EPOCH)173
.unwrap_or_default()174
.as_millis()175
.try_into()176
.unwrap_or(u64::MAX)177
}178
179
pub fn local_time_string(unix_millis: u64) -> Option<String> {180
let ticks = unix_millis181
.checked_mul(TICKS_PER_MILLISECOND)?182
.checked_add(WINDOWS_EPOCH_OFFSET_TICKS)?;183
let file_time = FILETIME {184
dwLowDateTime: ticks as u32,185
dwHighDateTime: (ticks >> 32) as u32,186
};187
let mut utc = SYSTEMTIME::default();188
unsafe { FileTimeToSystemTime(&file_time, &mut utc) }.ok()?;189
let mut local = SYSTEMTIME::default();190
unsafe { SystemTimeToTzSpecificLocalTimeEx(None, &utc, &mut local) }.ok()?;191
Some(format!(192
"{:04}-{:02}-{:02} {:02}:{:02}:{:02}.{:03}",193
local.wYear,194
local.wMonth,195
local.wDay,196
local.wHour,197
local.wMinute,198
local.wSecond,199
local.wMilliseconds200
))201
}202
203
struct Diagnostics {204
path: PathBuf,205
max_file_bytes: u64,206
memory_records: usize,207
state: Mutex<State>,208
}209
210
struct State {211
recent: VecDeque<Record>,212
}213
214
#[derive(Default)]215
struct CleanupReport {216
removed: usize,217
failures: Vec<String>,218
}219
220
struct CrossProcessLogGuard(HANDLE);221
222
impl CrossProcessLogGuard {223
fn acquire() -> io::Result<Self> {224
let handle = unsafe { CreateMutexW(None, false, w!("Local\\SBMSDiagnostics-v1")) }225
.map_err(io::Error::other)?;226
let wait = unsafe { WaitForSingleObject(handle, INFINITE) };227
if wait == WAIT_OBJECT_0 || wait == WAIT_ABANDONED {228
return Ok(Self(handle));229
}230
231
unsafe {232
let _ = CloseHandle(handle);233
}234
if wait == WAIT_FAILED {235
Err(io::Error::last_os_error())236
} else {237
Err(io::Error::other(format!(238
"unexpected diagnostics mutex wait result: {}",239
wait.0240
)))241
}242
}243
}244
245
impl Drop for CrossProcessLogGuard {246
fn drop(&mut self) {247
unsafe {248
let _ = ReleaseMutex(self.0);249
let _ = CloseHandle(self.0);250
}251
}252
}253
254
impl Diagnostics {255
fn open(path: PathBuf, max_file_bytes: u64, memory_records: usize) -> io::Result<Self> {256
if max_file_bytes == 0 {257
return Err(io::Error::new(258
io::ErrorKind::InvalidInput,259
"maximum log file size must be greater than zero",260
));261
}262
if memory_records == 0 {263
return Err(io::Error::new(264
io::ErrorKind::InvalidInput,265
"memory record capacity must be greater than zero",266
));267
}268
if let Some(parent) = path.parent() {269
fs::create_dir_all(parent)?;270
}271
let cleanup_report = match CrossProcessLogGuard::acquire() {272
Ok(_guard) => cleanup_obsolete_logs(273
path.parent().unwrap_or_else(|| Path::new(".")),274
SystemTime::now(),275
),276
Err(error) => CleanupReport {277
failures: vec![format!("could not acquire the diagnostics lock: {error}")],278
..CleanupReport::default()279
},280
};281
let diagnostics = Self {282
path,283
max_file_bytes,284
memory_records,285
state: Mutex::new(State {286
recent: VecDeque::with_capacity(memory_records),287
}),288
};289
diagnostics.report_cleanup(cleanup_report);290
Ok(diagnostics)291
}292
293
fn write(&self, mut record: Record) -> io::Result<()> {294
let line = encode_line(&mut record, self.max_file_bytes as usize)?;295
let mut state = self296
.state297
.lock()298
.unwrap_or_else(|poisoned| poisoned.into_inner());299
300
if state.recent.len() == self.memory_records {301
state.recent.pop_front();302
}303
state.recent.push_back(record.clone());304
305
let _guard = CrossProcessLogGuard::acquire()?;306
let current_bytes = OpenOptions::new()307
.create(true)308
.append(true)309
.open(&self.path)?310
.metadata()?311
.len();312
if current_bytes > 0313
&& current_bytes.saturating_add(line.len() as u64) > self.max_file_bytes314
{315
self.rotate()?;316
}317
318
// Open the active path for every record. A persistent handle can keep pointing at319
// `sbms.log.1` after another SBMS process rotates the file.320
let mut file = OpenOptions::new()321
.create(true)322
.append(true)323
.open(&self.path)?;324
file.write_all(&line)?;325
file.flush()?;326
if record.level == Level::Error {327
file.sync_data()?;328
}329
Ok(())330
}331
332
fn report_cleanup(&self, report: CleanupReport) {333
if report.removed > 0 {334
let time = unix_time_millis();335
let _ = self.write(Record {336
time,337
local_time: local_time_string(time).unwrap_or_default(),338
level: Level::Info,339
module: "diagnostics".into(),340
stage: "retention".into(),341
mapping_session_id: "-".into(),342
message: format!(343
"removed {} obsolete or expired legacy log file(s)",344
report.removed345
),346
});347
}348
if !report.failures.is_empty() {349
let time = unix_time_millis();350
let failure_count = report.failures.len();351
let details = report352
.failures353
.iter()354
.take(5)355
.cloned()356
.collect::<Vec<_>>()357
.join("; ");358
let _ = self.write(Record {359
time,360
local_time: local_time_string(time).unwrap_or_default(),361
level: Level::Warn,362
module: "diagnostics".into(),363
stage: "retention".into(),364
mapping_session_id: "-".into(),365
message: format!("could not clean {failure_count} legacy log file(s): {details}"),366
});367
}368
}369
370
fn recent(&self) -> Vec<Record> {371
self.state372
.lock()373
.unwrap_or_else(|poisoned| poisoned.into_inner())374
.recent375
.iter()376
.cloned()377
.collect()378
}379
380
fn rotate(&self) -> io::Result<()> {381
let oldest = rotated_path(&self.path, ROLLING_FILE_COUNT - 1);382
if oldest.exists() {383
fs::remove_file(&oldest)?;384
}385
for index in (1..ROLLING_FILE_COUNT - 1).rev() {386
let source = rotated_path(&self.path, index);387
if source.exists() {388
fs::rename(source, rotated_path(&self.path, index + 1))?;389
}390
}391
if self.path.exists() {392
fs::rename(&self.path, rotated_path(&self.path, 1))?;393
}394
Ok(())395
}396
}397
398
fn cleanup_obsolete_logs(directory: &Path, now: SystemTime) -> CleanupReport {399
let mut report = CleanupReport::default();400
let entries = match fs::read_dir(directory) {401
Ok(entries) => entries,402
Err(error) => {403
report404
.failures405
.push(format!("{}: {error}", directory.display()));406
return report;407
}408
};409
410
for entry in entries {411
let entry = match entry {412
Ok(entry) => entry,413
Err(error) => {414
report.failures.push(error.to_string());415
continue;416
}417
};418
let file_type = match entry.file_type() {419
Ok(file_type) => file_type,420
Err(error) => {421
report422
.failures423
.push(format!("{}: {error}", entry.path().display()));424
continue;425
}426
};427
if !file_type.is_file() {428
continue;429
}430
let Some(name) = entry.file_name().to_str().map(str::to_owned) else {431
continue;432
};433
434
let remove = is_obsolete_legacy_log(&name)435
|| (is_known_auxiliary_log(&name)436
&& match entry.metadata().and_then(|metadata| metadata.modified()) {437
Ok(modified) => now438
.duration_since(modified)439
.is_ok_and(|age| age >= AUXILIARY_LOG_RETENTION),440
Err(error) => {441
report442
.failures443
.push(format!("{}: {error}", entry.path().display()));444
false445
}446
});447
if !remove {448
continue;449
}450
451
match fs::remove_file(entry.path()) {452
Ok(()) => report.removed += 1,453
Err(error) if error.kind() == io::ErrorKind::NotFound => {}454
Err(error) => report455
.failures456
.push(format!("{}: {error}", entry.path().display())),457
}458
}459
report460
}461
462
fn is_obsolete_legacy_log(name: &str) -> bool {463
name.eq_ignore_ascii_case("latest.log")464
|| name.eq_ignore_ascii_case("error.log")465
|| exact_timestamped_log(name, "SBMS-")466
}467
468
fn is_known_auxiliary_log(name: &str) -> bool {469
if name.eq_ignore_ascii_case("sunshine-actions.log") {470
return true;471
}472
exact_timestamped_log(name, "setup-")473
|| [474
"elevated-",475
"native-smoke-",476
"programfiles-sync-",477
"ab-driver-verify-",478
]479
.iter()480
.any(|prefix| timestamp_suffixed_log(name, prefix))481
}482
483
fn exact_timestamped_log(name: &str, prefix: &str) -> bool {484
let Some(stem) = name485
.strip_prefix(prefix)486
.and_then(|value| value.strip_suffix(".log"))487
else {488
return false;489
};490
is_log_timestamp(stem)491
}492
493
fn timestamp_suffixed_log(name: &str, prefix: &str) -> bool {494
let Some(stem) = name495
.strip_prefix(prefix)496
.and_then(|value| value.strip_suffix(".log"))497
else {498
return false;499
};500
stem.get(stem.len().saturating_sub(15)..)501
.is_some_and(is_log_timestamp)502
}503
504
fn is_log_timestamp(value: &str) -> bool {505
value.len() == 15506
&& value.as_bytes()[8] == b'-'507
&& value508
.bytes()509
.enumerate()510
.all(|(index, byte)| index == 8 || byte.is_ascii_digit())511
}512
513
fn encode_line(record: &mut Record, max_bytes: usize) -> io::Result<Vec<u8>> {514
let mut line = serde_json::to_vec(record).map_err(io::Error::other)?;515
line.push(b'\n');516
if line.len() <= max_bytes {517
return Ok(line);518
}519
520
record.message.clear();521
let fixed_bytes = serde_json::to_vec(record)522
.map_err(io::Error::other)?523
.len()524
.saturating_add(1);525
if fixed_bytes > max_bytes {526
return Err(io::Error::new(527
io::ErrorKind::InvalidInput,528
"maximum log file size is too small for a diagnostic record",529
));530
}531
Ok(serde_json::to_vec(record)532
.map_err(io::Error::other)?533
.into_iter()534
.chain(std::iter::once(b'\n'))535
.collect())536
}537
538
fn rotated_path(path: &Path, index: usize) -> PathBuf {539
let mut name = path540
.file_name()541
.map(OsString::from)542
.unwrap_or_else(|| OsString::from("sbms.log"));543
name.push(format!(".{index}"));544
path.with_file_name(name)545
}546
547
#[cfg(test)]548
mod tests {549
use super::*;550
551
static NEXT_TEST_DIR: AtomicU64 = AtomicU64::new(1);552
553
fn test_path(name: &str) -> PathBuf {554
std::env::temp_dir()555
.join(format!(556
"sbms-diagnostics-{}-{}",557
std::process::id(),558
NEXT_TEST_DIR.fetch_add(1, Ordering::Relaxed)559
))560
.join(name)561
}562
563
fn record(index: usize, level: Level) -> Record {564
Record {565
time: index as u64,566
local_time: String::new(),567
level,568
module: "test".into(),569
stage: "write".into(),570
mapping_session_id: "1-1".into(),571
message: format!("record-{index}-{}", "x".repeat(96)),572
}573
}574
575
#[test]576
fn ring_buffer_keeps_the_latest_records() {577
let path = test_path("sbms.log");578
let diagnostics = Diagnostics::open(path.clone(), 1024 * 1024, 200).unwrap();579
for index in 0..225 {580
diagnostics.write(record(index, Level::Info)).unwrap();581
}582
let recent = diagnostics.recent();583
assert_eq!(recent.len(), 200);584
assert_eq!(recent.first().unwrap().time, 25);585
assert_eq!(recent.last().unwrap().time, 224);586
drop(diagnostics);587
fs::remove_dir_all(path.parent().unwrap()).unwrap();588
}589
590
#[test]591
fn old_records_without_local_time_remain_compatible() {592
let record: Record = serde_json::from_str(593
r#"{"time":1,"level":"INFO","module":"test","stage":"read","mapping_session_id":"-","message":"legacy"}"#,594
)595
.unwrap();596
597
assert_eq!(record.time, 1);598
assert!(record.local_time.is_empty());599
}600
601
#[test]602
fn local_time_uses_a_readable_system_time_shape() {603
let time = local_time_string(unix_time_millis()).unwrap();604
605
assert_eq!(time.len(), 23);606
assert_eq!(&time[4..5], "-");607
assert_eq!(&time[10..11], " ");608
assert_eq!(&time[19..20], ".");609
}610
611
#[test]612
fn rotation_keeps_three_bounded_files() {613
let path = test_path("sbms.log");614
let diagnostics = Diagnostics::open(path.clone(), 512, 200).unwrap();615
for index in 0..30 {616
diagnostics.write(record(index, Level::Error)).unwrap();617
}618
drop(diagnostics);619
620
for candidate in [&path, &rotated_path(&path, 1), &rotated_path(&path, 2)] {621
assert!(candidate.is_file());622
assert!(candidate.metadata().unwrap().len() <= 512);623
}624
assert!(!rotated_path(&path, 3).exists());625
fs::remove_dir_all(path.parent().unwrap()).unwrap();626
}627
628
#[test]629
fn rotation_coordinates_multiple_diagnostics_instances() {630
let path = test_path("sbms.log");631
let first = Diagnostics::open(path.clone(), 512, 200).unwrap();632
let second = Diagnostics::open(path.clone(), 512, 200).unwrap();633
for index in 0..30 {634
let diagnostics = if index % 2 == 0 { &first } else { &second };635
diagnostics.write(record(index, Level::Info)).unwrap();636
}637
let mut latest = record(31, Level::Info);638
latest.message = "latest-after-cross-process-rotation".into();639
first.write(latest).unwrap();640
641
assert!(642
fs::read_to_string(&path)643
.unwrap()644
.contains("latest-after-cross-process-rotation")645
);646
for candidate in [&path, &rotated_path(&path, 1), &rotated_path(&path, 2)] {647
assert!(candidate.metadata().unwrap().len() <= 512);648
}649
assert!(!rotated_path(&path, 3).exists());650
drop((first, second));651
fs::remove_dir_all(path.parent().unwrap()).unwrap();652
}653
654
#[test]655
fn legacy_cleanup_removes_only_explicit_obsolete_names() {656
let path = test_path("sbms.log");657
let directory = path.parent().unwrap();658
fs::create_dir_all(directory).unwrap();659
for name in [660
"SBMS-20260727-054400.log",661
"latest.log",662
"error.log",663
"setup-20260813-120000.log",664
"SBMS-not-a-timestamp.log",665
"SBMS-user-20260727-054400.log",666
"user-notes.log",667
"sbms.log",668
"sbms.log.1",669
"sbms.log.2",670
] {671
fs::write(directory.join(name), name).unwrap();672
}673
674
let report = cleanup_obsolete_logs(directory, SystemTime::now());675
676
assert_eq!(report.removed, 3);677
assert!(report.failures.is_empty());678
for name in [679
"setup-20260813-120000.log",680
"SBMS-not-a-timestamp.log",681
"SBMS-user-20260727-054400.log",682
"user-notes.log",683
"sbms.log",684
"sbms.log.1",685
"sbms.log.2",686
] {687
assert!(directory.join(name).is_file(), "{name} should be preserved");688
}689
fs::remove_dir_all(directory).unwrap();690
}691
692
#[test]693
fn auxiliary_cleanup_applies_thirty_day_retention() {694
let path = test_path("sbms.log");695
let directory = path.parent().unwrap();696
fs::create_dir_all(directory).unwrap();697
for name in [698
"setup-20260629-220847.log",699
"elevated-gui-start-smoke-063-20260629-122148.log",700
"native-smoke-20260629-043731.log",701
"programfiles-sync-20260629-043721.log",702
"ab-driver-verify-20260629-043305.log",703
"sunshine-actions.log",704
"unknown-20260629-043305.log",705
"sbms.log",706
] {707
fs::write(directory.join(name), name).unwrap();708
}709
let after_retention = SystemTime::now() + AUXILIARY_LOG_RETENTION + Duration::from_secs(1);710
711
let report = cleanup_obsolete_logs(directory, after_retention);712
713
assert_eq!(report.removed, 6);714
assert!(report.failures.is_empty());715
assert!(directory.join("unknown-20260629-043305.log").is_file());716
assert!(directory.join("sbms.log").is_file());717
fs::remove_dir_all(directory).unwrap();718
}719
720
#[test]721
fn error_is_visible_without_dropping_the_writer() {722
let path = test_path("sbms.log");723
let diagnostics = Diagnostics::open(path.clone(), 1024 * 1024, 200).unwrap();724
diagnostics.write(record(1, Level::Info)).unwrap();725
diagnostics.write(record(2, Level::Error)).unwrap();726
727
let text = fs::read_to_string(&path).unwrap();728
assert!(text.contains("\"level\":\"ERROR\""));729
assert!(text.contains("record-2"));730
drop(diagnostics);731
fs::remove_dir_all(path.parent().unwrap()).unwrap();732
}733
734
#[test]735
fn persisted_recent_skips_malformed_lines_and_keeps_the_latest_records() {736
let path = test_path("sbms.log");737
fs::create_dir_all(path.parent().unwrap()).unwrap();738
let mut contents = String::new();739
for index in 0..205 {740
contents.push_str(&serde_json::to_string(&record(index, Level::Info)).unwrap());741
contents.push('\n');742
}743
contents.push_str("not-json\n");744
fs::write(&path, contents).unwrap();745
746
let records = {747
let _guard = CrossProcessLogGuard::acquire().unwrap();748
let mut records = VecDeque::with_capacity(DEFAULT_MEMORY_RECORDS);749
let file = fs::File::open(&path).unwrap();750
for line in BufReader::new(file).lines() {751
let line = line.unwrap();752
let Ok(record) = serde_json::from_str::<Record>(&line) else {753
continue;754
};755
if records.len() == DEFAULT_MEMORY_RECORDS {756
records.pop_front();757
}758
records.push_back(record);759
}760
records761
};762
763
assert_eq!(records.len(), 200);764
assert_eq!(records.front().unwrap().time, 5);765
assert_eq!(records.back().unwrap().time, 204);766
fs::remove_dir_all(path.parent().unwrap()).unwrap();767
}768
769
#[test]770
fn mapping_session_ids_are_unique() {771
assert_ne!(new_mapping_session_id(), new_mapping_session_id());772
}773
}774