execution.rs
1 use crate::event::TimedEvent; 2 use crate::gfx::Rgba8; 3 use crate::util; 4 5 use std::collections::VecDeque; 6 use std::fmt; 7 use std::fs::File; 8 use std::io; 9 use std::io::{BufRead, Write}; 10 use std::path::{Path, PathBuf}; 11 use std::str::FromStr; 12 use std::time; 13 14 use seahash::SeaHasher; 15 16 #[derive(Debug, Clone, Eq, PartialEq)] 17 pub enum GifMode { 18 Ignore, 19 Record, 20 } 21 22 /// Determines whether frame digests are recorded, verified or ignored. 23 #[derive(Debug, Clone, Copy, PartialEq, Eq)] 24 pub enum DigestMode { 25 /// Verify digests. 26 Verify, 27 /// Record digests. 28 Record, 29 /// Ignore digest. 30 Ignore, 31 } 32 33 pub struct DigestState { 34 pub mode: DigestMode, 35 pub path: Option<PathBuf>, 36 } 37 38 impl DigestState { 39 pub fn from<P: AsRef<Path>>(mode: DigestMode, path: P) -> io::Result<Self> { 40 match mode { 41 DigestMode::Verify => Self::verify(path), 42 DigestMode::Record => Self::record(path), 43 DigestMode::Ignore => Self::ignore(), 44 } 45 } 46 47 pub fn verify<P: AsRef<Path>>(path: P) -> io::Result<Self> { 48 let mut frames = Vec::new(); 49 let path = path.as_ref(); 50 51 match File::open(path) { 52 Ok(f) => { 53 let r = io::BufReader::new(f); 54 for line in r.lines() { 55 let line = line?; 56 let hash = Hash::from_str(line.as_str()) 57 .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?; 58 frames.push(hash); 59 } 60 } 61 Err(e) => { 62 return Err(io::Error::new( 63 e.kind(), 64 format!("{}: {}", path.display(), e), 65 )); 66 } 67 } 68 69 Ok(Self { 70 mode: DigestMode::Verify, 71 path: Some(path.into()), 72 }) 73 } 74 75 pub fn record<P: AsRef<Path>>(path: P) -> io::Result<Self> { 76 Ok(Self { 77 mode: DigestMode::Record, 78 path: Some(path.as_ref().into()), 79 }) 80 } 81 82 pub fn ignore() -> io::Result<Self> { 83 Ok(Self { 84 mode: DigestMode::Ignore, 85 path: None, 86 }) 87 } 88 } 89 90 #[derive(Debug, Clone)] 91 pub enum ExecutionMode { 92 Normal, 93 Record(PathBuf, DigestMode, GifMode), 94 Replay(PathBuf, DigestMode), 95 } 96 97 /// Execution mode. Controls whether the session is playing or recording 98 /// commands. 99 // TODO: Make this a `struct` and have `ExecutionMode`. 100 #[derive(Default)] 101 pub enum Execution { 102 /// Normal execution. User inputs are processed normally. 103 #[default] 104 Normal, 105 /// Recording user inputs to log. 106 Recording { 107 /// Events being recorded. 108 events: Vec<TimedEvent>, 109 /// Start time of recording. 110 start: time::Instant, 111 /// Path to save recording to. 112 path: PathBuf, 113 /// Digest mode. 114 digest: DigestState, 115 /// Frame recorder. 116 recorder: FrameRecorder, 117 }, 118 /// Replaying inputs from log. 119 Replaying { 120 /// Events being replayed. 121 events: VecDeque<TimedEvent>, 122 /// Start time of the playback. 123 start: time::Instant, 124 /// Path to read events from. 125 path: PathBuf, 126 /// Digest mode. 127 digest: DigestState, 128 /// Replay result. 129 result: ReplayResult, 130 /// Frame recorder. 131 recorder: FrameRecorder, 132 }, 133 } 134 135 impl Execution { 136 /// Create a normal execution. 137 pub fn normal() -> io::Result<Self> { 138 Ok(Self::Normal) 139 } 140 141 /// Create a recording. 142 pub fn recording<P: AsRef<Path>>( 143 path: P, 144 digest_mode: DigestMode, 145 w: u16, 146 h: u16, 147 gif_mode: GifMode, 148 ) -> io::Result<Self> { 149 use io::{Error, ErrorKind}; 150 151 let path = path.as_ref(); 152 let file_name: &Path = path 153 .file_name() 154 .ok_or(Error::new( 155 ErrorKind::InvalidInput, 156 format!("invalid path {:?}", path), 157 ))? 158 .as_ref(); 159 160 std::fs::create_dir_all(path)?; 161 162 let digest = DigestState::from(digest_mode, path.join(file_name).with_extension("digest"))?; 163 let gif_recorder = if gif_mode == GifMode::Record { 164 GifRecorder::new(path.join(file_name).with_extension("gif"), w, h)? 165 } else { 166 GifRecorder::dummy() 167 }; 168 let recorder = FrameRecorder::new(gif_recorder, gif_mode, digest_mode); 169 170 Ok(Self::Recording { 171 events: Vec::new(), 172 start: time::Instant::now(), 173 path: path.to_path_buf(), 174 digest, 175 recorder, 176 }) 177 } 178 179 /// Create a replay. 180 pub fn replaying<P: AsRef<Path>>(path: P, mode: DigestMode) -> io::Result<Self> { 181 use io::{Error, ErrorKind}; 182 183 let mut events = VecDeque::new(); 184 let path = path.as_ref(); 185 186 let file_name: &Path = path 187 .file_name() 188 .ok_or(Error::new( 189 ErrorKind::InvalidInput, 190 format!("invalid path {:?}", path), 191 ))? 192 .as_ref(); 193 194 let digest = DigestState::from(mode, path.join(file_name).with_extension("digest"))?; 195 196 let recorder = match &digest { 197 DigestState { 198 path: Some(path), 199 mode: DigestMode::Verify, 200 } => { 201 let mut frames = Vec::new(); 202 203 match File::open(path) { 204 Ok(f) => { 205 let r = io::BufReader::new(f); 206 for line in r.lines() { 207 let line = line?; 208 let hash = Hash::from_str(line.as_str()) 209 .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?; 210 frames.push(hash); 211 } 212 } 213 Err(e) => { 214 return Err(io::Error::new( 215 e.kind(), 216 format!("{}: {}", path.display(), e), 217 )); 218 } 219 } 220 FrameRecorder::from(frames, mode) 221 } 222 _ => FrameRecorder::new(GifRecorder::dummy(), GifMode::Ignore, mode), 223 }; 224 225 let events_path = path.join(file_name).with_extension("events"); 226 match File::open(&events_path) { 227 Ok(f) => { 228 let r = io::BufReader::new(f); 229 for (i, line) in r.lines().enumerate() { 230 let line = line?; 231 let ev = TimedEvent::from_str(&line).map_err(|e| { 232 io::Error::new( 233 io::ErrorKind::InvalidInput, 234 format!("{}:{}: {}", events_path.display(), i + 1, e), 235 ) 236 })?; 237 events.push_back(ev); 238 } 239 Ok(Self::Replaying { 240 events, 241 start: time::Instant::now(), 242 path: path.to_path_buf(), 243 digest, 244 result: ReplayResult::new(), 245 recorder, 246 }) 247 } 248 Err(e) => Err(io::Error::new( 249 e.kind(), 250 format!("{}: {}", events_path.display(), e), 251 )), 252 } 253 } 254 255 pub fn is_normal(&self) -> bool { 256 matches!(self, Execution::Normal) 257 } 258 259 pub fn is_recording(&self) -> bool { 260 matches!(self, Execution::Recording { .. }) 261 } 262 263 pub fn record(&mut self, data: &[Rgba8]) -> Result<(), VerifyResult> { 264 match self { 265 // Replaying and verifying digests. 266 Self::Replaying { 267 digest: 268 DigestState { 269 mode: DigestMode::Verify, 270 .. 271 }, 272 result, 273 recorder, 274 .. 275 } => { 276 let vr = recorder.verify_frame(data); 277 result.record(&vr); 278 279 if vr.is_err() { 280 return Err(vr); 281 } 282 } 283 // Replaying/Recording and recording digests. 284 Self::Replaying { recorder, .. } | Self::Recording { recorder, .. } => { 285 recorder.record_frame(data); 286 } 287 // In normal mode, we don't actual record anything. 288 _ => {} 289 } 290 Ok(()) 291 } 292 293 //////////////////////////////////////////////////////////////////////////// 294 295 pub fn stop_recording(&mut self) -> io::Result<PathBuf> { 296 use io::{Error, ErrorKind}; 297 298 let result = if let Execution::Recording { 299 events, 300 path, 301 digest, 302 recorder, 303 .. 304 } = self 305 { 306 recorder.finish()?; 307 308 let file_name: &Path = path 309 .file_name() 310 .ok_or(Error::new( 311 ErrorKind::InvalidInput, 312 format!("invalid path {:?}", path), 313 ))? 314 .as_ref(); 315 316 let mut f = File::create(path.join(file_name.with_extension("events")))?; 317 for ev in events.clone() { 318 writeln!(&mut f, "{}", String::from(ev))?; 319 } 320 321 if let DigestState { 322 mode: DigestMode::Record, 323 path: Some(path), 324 .. 325 } = digest 326 { 327 Execution::write_digest(recorder, path)?; 328 } 329 330 Ok(path.clone()) 331 } else { 332 panic!("record finalizer called outside of recording context") 333 }; 334 335 if result.is_ok() { 336 *self = Execution::Normal; 337 } 338 result 339 } 340 341 pub fn finalize_replaying(&self) -> io::Result<PathBuf> { 342 if let Execution::Replaying { 343 digest: 344 DigestState { 345 mode: DigestMode::Record, 346 path: Some(path), 347 .. 348 }, 349 recorder, 350 .. 351 } = &self 352 { 353 Execution::write_digest(recorder, path)?; 354 Ok(path.clone()) 355 } else { 356 panic!("replay finalizer called outside of replay context") 357 } 358 } 359 360 //////////////////////////////////////////////////////////////////////////// 361 362 fn write_digest<P: AsRef<Path>>(recorder: &FrameRecorder, path: P) -> io::Result<()> { 363 let path = path.as_ref(); 364 365 if let Some(parent) = path.parent() { 366 std::fs::create_dir_all(parent)?; 367 } 368 let mut f = File::create(path)?; 369 370 for frame in &recorder.frames { 371 writeln!(&mut f, "{}", frame)?; 372 } 373 Ok(()) 374 } 375 } 376 377 pub struct GifRecorder { 378 width: u16, 379 height: u16, 380 encoder: Option<gif::Encoder<Box<File>>>, 381 frames: Vec<(time::Instant, Vec<Rgba8>)>, 382 } 383 384 impl GifRecorder { 385 const GIF_ENCODING_SPEED: i32 = 30; 386 387 pub fn new<P: AsRef<Path>>(path: P, width: u16, height: u16) -> io::Result<Self> { 388 let file = Box::new(File::create(path.as_ref())?); 389 let encoder = Some(gif::Encoder::new(file, width, height, &[])?); 390 391 Ok(Self { 392 width, 393 height, 394 encoder, 395 frames: Vec::new(), 396 }) 397 } 398 399 fn dummy() -> Self { 400 Self { 401 width: 0, 402 height: 0, 403 encoder: None, 404 frames: Vec::new(), 405 } 406 } 407 408 fn is_dummy(&self) -> bool { 409 self.width == 0 && self.height == 0 410 } 411 412 fn record(&mut self, data: &[Rgba8]) { 413 if self.is_dummy() { 414 return; 415 } 416 let now = time::Instant::now(); 417 418 self.frames.push((now, data.to_vec())); 419 } 420 421 fn finish(&mut self) -> io::Result<()> { 422 if let Some(encoder) = &mut self.encoder { 423 for (i, (t1, gif_data)) in self.frames.iter().enumerate() { 424 let delay = if let Some((t2, _)) = self.frames.get(i + 1) { 425 *t2 - *t1 426 } else { 427 // Let the last frame linger for a second... 428 time::Duration::from_secs(1) 429 }; 430 431 let data = util::align_u8(gif_data); 432 let mut frame = gif::Frame::from_rgb_speed( 433 self.width, 434 self.height, 435 data, 436 Self::GIF_ENCODING_SPEED, 437 ); 438 frame.dispose = gif::DisposalMethod::Background; 439 frame.delay = (delay.as_millis() / 10) 440 .try_into() 441 .expect("`delay` is not an unreasonably large number"); 442 443 encoder.write_frame(&frame)?; 444 } 445 } 446 Ok(()) 447 } 448 } 449 450 /// Records and verifies frames being replayed. 451 pub struct FrameRecorder { 452 frames: VecDeque<Hash>, 453 last_verified: Option<Hash>, 454 gif_recorder: GifRecorder, 455 gif_mode: GifMode, 456 digest_mode: DigestMode, 457 } 458 459 impl FrameRecorder { 460 fn new(gif_recorder: GifRecorder, gif_mode: GifMode, digest_mode: DigestMode) -> Self { 461 Self { 462 frames: VecDeque::new(), 463 last_verified: None, 464 gif_recorder, 465 gif_mode, 466 digest_mode, 467 } 468 } 469 470 fn from(frames: Vec<Hash>, digest_mode: DigestMode) -> Self { 471 Self { 472 frames: frames.into(), 473 last_verified: None, 474 gif_recorder: GifRecorder::dummy(), 475 gif_mode: GifMode::Ignore, 476 digest_mode, 477 } 478 } 479 480 fn record_frame(&mut self, data: &[Rgba8]) { 481 let hash = Self::hash(data); 482 483 if self.frames.back().map(|h| h != &hash).unwrap_or(true) { 484 debug!("frame: {}", hash); 485 486 if self.digest_mode == DigestMode::Record || self.gif_mode == GifMode::Record { 487 self.frames.push_back(hash); 488 } 489 490 self.gif_recorder.record(data); 491 } 492 } 493 494 fn verify_frame(&mut self, data: &[Rgba8]) -> VerifyResult { 495 let actual = Self::hash(data); 496 497 if self.frames.is_empty() { 498 return VerifyResult::Eof; 499 } 500 if Some(actual.clone()) == self.last_verified { 501 return VerifyResult::Stale(actual); 502 } 503 self.last_verified = Some(actual.clone()); 504 505 if let Some(expected) = self.frames.pop_front() { 506 if actual == expected { 507 VerifyResult::Okay(actual) 508 } else { 509 VerifyResult::Failed(actual, expected) 510 } 511 } else { 512 VerifyResult::Eof 513 } 514 } 515 516 fn finish(&mut self) -> io::Result<()> { 517 self.gif_recorder.finish() 518 } 519 520 fn hash(data: &[Rgba8]) -> Hash { 521 use std::hash::Hasher; 522 523 let mut hasher = SeaHasher::new(); 524 let data = util::align_u8(data); 525 526 hasher.write(data); 527 Hash(hasher.finish()) 528 } 529 } 530 531 #[derive(Debug, Clone)] 532 pub struct ReplayResult { 533 eof: bool, 534 failed: bool, 535 okay_count: u32, 536 stale_count: u32, 537 } 538 539 impl ReplayResult { 540 pub fn is_ok(&self) -> bool { 541 !self.failed 542 } 543 544 pub fn is_err(&self) -> bool { 545 !self.is_ok() 546 } 547 548 pub fn is_done(&self) -> bool { 549 self.eof 550 } 551 552 pub fn summary(&self) -> String { 553 if self.is_err() { 554 format!("replay failed after {} frames", self.okay_count) 555 } else { 556 format!("ok ({} frames)", self.okay_count) 557 } 558 } 559 560 //////////////////////////////////////////////////////////////////////////// 561 562 fn new() -> Self { 563 ReplayResult { 564 okay_count: 0, 565 stale_count: 0, 566 failed: false, 567 eof: false, 568 } 569 } 570 571 fn record(&mut self, result: &VerifyResult) { 572 match result { 573 VerifyResult::Okay(actual) => { 574 info!("verify: {} OK", actual); 575 self.okay_count += 1; 576 } 577 VerifyResult::Failed(actual, expected) => { 578 error!("verify: {} != {}", actual, expected); 579 self.failed = true; 580 } 581 VerifyResult::Eof => { 582 self.eof = true; 583 } 584 VerifyResult::Stale { .. } => { 585 self.stale_count += 1; 586 } 587 } 588 } 589 } 590 591 #[derive(Debug, Clone)] 592 pub enum VerifyResult { 593 /// The hash has already been verified. 594 Stale(Hash), 595 /// The actual and expected hashes match. 596 Okay(Hash), 597 /// The actual and expected hashes don't match. 598 Failed(Hash, Hash), 599 /// There are no further expected hashes. 600 Eof, 601 } 602 603 impl VerifyResult { 604 fn is_err(&self) -> bool { 605 matches!(self, Self::Failed(_, _)) 606 } 607 } 608 609 #[derive(PartialEq, Eq, Clone, Debug)] 610 pub struct Hash(u64); 611 612 impl fmt::Display for Hash { 613 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 614 write!(f, "{:016x}", self.0) 615 } 616 } 617 618 impl FromStr for Hash { 619 type Err = std::num::ParseIntError; 620 621 fn from_str(input: &str) -> Result<Self, Self::Err> { 622 u64::from_str_radix(input, 16).map(Hash) 623 } 624 }