1use serde::{Deserialize, Serialize};
4use std::sync::Arc;
5use zccache_core::NormalizedPath;
6
7#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
9pub enum Request {
10 Ping,
12 Shutdown,
14 Status,
16 Lookup {
18 cache_key: String,
20 },
21 Store {
23 cache_key: String,
25 artifact: ArtifactData,
27 },
28 SessionStart {
30 client_pid: u32,
32 working_dir: NormalizedPath,
34 log_file: Option<NormalizedPath>,
36 track_stats: bool,
38 journal_path: Option<NormalizedPath>,
40 },
41 Compile {
43 session_id: String,
45 args: Vec<String>,
47 cwd: NormalizedPath,
49 compiler: NormalizedPath,
51 env: Option<Vec<(String, String)>>,
55 stdin: Vec<u8>,
61 },
62 SessionEnd {
64 session_id: String,
66 },
67 Clear,
69 CompileEphemeral {
73 client_pid: u32,
75 working_dir: NormalizedPath,
77 compiler: NormalizedPath,
79 args: Vec<String>,
81 cwd: NormalizedPath,
83 env: Option<Vec<(String, String)>>,
85 stdin: Vec<u8>,
89 },
90 LinkEphemeral {
93 client_pid: u32,
95 tool: NormalizedPath,
97 args: Vec<String>,
99 cwd: NormalizedPath,
101 env: Option<Vec<(String, String)>>,
103 },
104 SessionStats {
107 session_id: String,
109 },
110 FingerprintCheck {
113 cache_file: NormalizedPath,
115 cache_type: String,
117 root: NormalizedPath,
119 extensions: Vec<String>,
122 include_globs: Vec<String>,
125 exclude: Vec<String>,
127 },
128 FingerprintMarkSuccess {
130 cache_file: NormalizedPath,
132 },
133 FingerprintMarkFailure {
135 cache_file: NormalizedPath,
137 },
138 FingerprintInvalidate {
140 cache_file: NormalizedPath,
142 },
143 ListRustArtifacts,
146}
147
148#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
150pub enum Response {
151 Pong,
153 ShuttingDown,
155 Status(DaemonStatus),
157 LookupResult(LookupResult),
159 StoreResult(StoreResult),
161 SessionStarted {
163 session_id: String,
165 journal_path: Option<NormalizedPath>,
167 },
168 CompileResult {
170 exit_code: i32,
172 stdout: Arc<Vec<u8>>,
174 stderr: Arc<Vec<u8>>,
176 cached: bool,
178 },
179 SessionEnded {
181 stats: Option<SessionStats>,
183 },
184 LinkResult {
186 exit_code: i32,
188 stdout: Arc<Vec<u8>>,
190 stderr: Arc<Vec<u8>>,
192 cached: bool,
194 warning: Option<String>,
196 },
197 Error {
199 message: String,
201 },
202 Cleared {
204 artifacts_removed: u64,
206 metadata_cleared: u64,
208 dep_graph_contexts_cleared: u64,
210 on_disk_bytes_freed: u64,
212 },
213 SessionStatsResult {
216 stats: Option<SessionStats>,
218 },
219 FingerprintCheckResult {
222 decision: String,
224 reason: Option<String>,
226 changed_files: Vec<String>,
228 },
229 FingerprintAck,
231 RustArtifactList { artifacts: Vec<RustArtifactInfo> },
234}
235
236#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
238pub struct DaemonStatus {
239 pub version: String,
241 pub artifact_count: u64,
243 pub cache_size_bytes: u64,
245 pub metadata_entries: u64,
247 pub uptime_secs: u64,
249 pub cache_hits: u64,
251 pub cache_misses: u64,
253 pub total_compilations: u64,
255 pub non_cacheable: u64,
257 pub compile_errors: u64,
259 pub time_saved_ms: u64,
261 pub total_links: u64,
263 pub link_hits: u64,
265 pub link_misses: u64,
267 pub link_non_cacheable: u64,
269 pub dep_graph_contexts: u64,
271 pub dep_graph_files: u64,
273 pub sessions_total: u64,
275 pub sessions_active: u64,
277 pub cache_dir: NormalizedPath,
279 pub dep_graph_version: u32,
281 pub dep_graph_disk_size: u64,
283 pub dep_graph_persisted: bool,
289}
290
291#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
293pub enum LookupResult {
294 Hit {
296 artifact: ArtifactData,
298 },
299 Miss,
301}
302
303#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
305pub enum StoreResult {
306 Stored,
308 AlreadyExists,
310}
311
312#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
314pub struct ArtifactData {
315 pub outputs: Vec<ArtifactOutput>,
317 pub stdout: Arc<Vec<u8>>,
319 pub stderr: Arc<Vec<u8>>,
321 pub exit_code: i32,
323}
324
325#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
327pub struct SessionStats {
328 pub duration_ms: u64,
330 pub compilations: u64,
332 pub hits: u64,
334 pub misses: u64,
336 pub non_cacheable: u64,
338 pub errors: u64,
340 pub time_saved_ms: u64,
342 pub unique_sources: u64,
344 pub bytes_read: u64,
346 pub bytes_written: u64,
348}
349
350#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
352pub struct ArtifactOutput {
353 pub name: String,
355 pub data: Arc<Vec<u8>>,
357}
358
359#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
361pub struct RustArtifactInfo {
362 pub cache_key: String,
364 pub output_names: Vec<String>,
366 pub payload_count: usize,
368}
369
370#[cfg(test)]
371mod tests {
372 use super::*;
373
374 fn roundtrip<T: Serialize + serde::de::DeserializeOwned + PartialEq + std::fmt::Debug>(
376 val: &T,
377 ) {
378 let bytes = bincode::serialize(val).unwrap();
379 let decoded: T = bincode::deserialize(&bytes).unwrap();
380 assert_eq!(*val, decoded);
381 }
382
383 #[test]
384 fn session_stats_roundtrip() {
385 let stats = SessionStats {
386 duration_ms: 12345,
387 compilations: 100,
388 hits: 80,
389 misses: 15,
390 non_cacheable: 5,
391 errors: 2,
392 time_saved_ms: 8000,
393 unique_sources: 42,
394 bytes_read: 1024 * 1024,
395 bytes_written: 512 * 1024,
396 };
397 roundtrip(&stats);
398 }
399
400 #[test]
401 fn session_stats_default_zeros() {
402 let stats = SessionStats {
403 duration_ms: 0,
404 compilations: 0,
405 hits: 0,
406 misses: 0,
407 non_cacheable: 0,
408 errors: 0,
409 time_saved_ms: 0,
410 unique_sources: 0,
411 bytes_read: 0,
412 bytes_written: 0,
413 };
414 roundtrip(&stats);
415 }
416
417 #[test]
418 fn daemon_status_expanded_roundtrip() {
419 let status = DaemonStatus {
420 version: env!("CARGO_PKG_VERSION").to_string(),
421 artifact_count: 892,
422 cache_size_bytes: 147_000_000,
423 metadata_entries: 5430,
424 uptime_secs: 8040,
425 cache_hits: 1089,
426 cache_misses: 143,
427 total_compilations: 1247,
428 non_cacheable: 15,
429 compile_errors: 3,
430 time_saved_ms: 750_000,
431 total_links: 50,
432 link_hits: 38,
433 link_misses: 10,
434 link_non_cacheable: 2,
435 dep_graph_contexts: 892,
436 dep_graph_files: 4201,
437 sessions_total: 41,
438 sessions_active: 3,
439 cache_dir: "/home/user/.zccache".into(),
440 dep_graph_version: 1,
441 dep_graph_disk_size: 2_500_000,
442 dep_graph_persisted: true,
443 };
444 roundtrip(&status);
445 }
446
447 #[test]
448 fn session_start_with_track_stats_roundtrip() {
449 let req = Request::SessionStart {
450 client_pid: 1234,
451 working_dir: "/home/user/project".into(),
452 log_file: None,
453 track_stats: true,
454 journal_path: None,
455 };
456 roundtrip(&req);
457
458 let req_no_stats = Request::SessionStart {
459 client_pid: 1234,
460 working_dir: "/home/user/project".into(),
461 log_file: None,
462 track_stats: false,
463 journal_path: None,
464 };
465 roundtrip(&req_no_stats);
466 }
467
468 #[test]
469 fn session_start_with_journal_path_roundtrip() {
470 let req = Request::SessionStart {
471 client_pid: 5678,
472 working_dir: "/home/user/project".into(),
473 log_file: None,
474 track_stats: false,
475 journal_path: Some("/tmp/build.jsonl".into()),
476 };
477 roundtrip(&req);
478
479 let req_no_journal = Request::SessionStart {
480 client_pid: 5678,
481 working_dir: "/home/user/project".into(),
482 log_file: None,
483 track_stats: false,
484 journal_path: None,
485 };
486 roundtrip(&req_no_journal);
487 }
488
489 #[test]
490 fn session_started_with_journal_path_roundtrip() {
491 let resp = Response::SessionStarted {
492 session_id: "550e8400-e29b-41d4-a716-446655440000".into(),
493 journal_path: Some("/home/user/.zccache/logs/sessions/test.jsonl".into()),
494 };
495 roundtrip(&resp);
496
497 let resp_no_journal = Response::SessionStarted {
498 session_id: "550e8400-e29b-41d4-a716-446655440000".into(),
499 journal_path: None,
500 };
501 roundtrip(&resp_no_journal);
502 }
503
504 #[test]
505 fn session_ended_with_stats_roundtrip() {
506 let stats = SessionStats {
507 duration_ms: 34000,
508 compilations: 32,
509 hits: 28,
510 misses: 3,
511 non_cacheable: 1,
512 errors: 0,
513 time_saved_ms: 8200,
514 unique_sources: 30,
515 bytes_read: 2_000_000,
516 bytes_written: 500_000,
517 };
518 let resp = Response::SessionEnded { stats: Some(stats) };
519 roundtrip(&resp);
520
521 let resp_no_stats = Response::SessionEnded { stats: None };
522 roundtrip(&resp_no_stats);
523 }
524
525 #[test]
526 fn clear_request_roundtrip() {
527 roundtrip(&Request::Clear);
528 }
529
530 #[test]
531 fn cleared_response_roundtrip() {
532 roundtrip(&Response::Cleared {
533 artifacts_removed: 42,
534 metadata_cleared: 100,
535 dep_graph_contexts_cleared: 25,
536 on_disk_bytes_freed: 1024 * 1024,
537 });
538 }
539
540 #[test]
541 fn compile_ephemeral_roundtrip() {
542 roundtrip(&Request::CompileEphemeral {
543 client_pid: 9876,
544 working_dir: "/home/user/project".into(),
545 compiler: "/usr/bin/clang++".into(),
546 args: vec!["-c".into(), "main.cpp".into(), "-o".into(), "main.o".into()],
547 cwd: "/home/user/project/build".into(),
548 env: Some(vec![("PATH".into(), "/usr/bin".into())]),
549 stdin: Vec::new(),
550 });
551 roundtrip(&Request::CompileEphemeral {
555 client_pid: 1,
556 working_dir: ".".into(),
557 compiler: "gcc".into(),
558 args: vec![],
559 cwd: ".".into(),
560 env: None,
561 stdin: b"hello\x00world\nbinary\xff\xfe".to_vec(),
562 });
563 }
564
565 #[test]
566 fn link_ephemeral_roundtrip() {
567 roundtrip(&Request::LinkEphemeral {
568 client_pid: 5555,
569 tool: "/usr/bin/ar".into(),
570 args: vec!["rcs".into(), "libfoo.a".into(), "a.o".into(), "b.o".into()],
571 cwd: "/home/user/project/build".into(),
572 env: Some(vec![("PATH".into(), "/usr/bin".into())]),
573 });
574 roundtrip(&Request::LinkEphemeral {
575 client_pid: 1,
576 tool: "lib.exe".into(),
577 args: vec!["/OUT:foo.lib".into(), "a.obj".into()],
578 cwd: ".".into(),
579 env: None,
580 });
581 }
582
583 #[test]
584 fn link_result_roundtrip() {
585 roundtrip(&Response::LinkResult {
586 exit_code: 0,
587 stdout: Arc::new(vec![]),
588 stderr: Arc::new(vec![]),
589 cached: true,
590 warning: None,
591 });
592 roundtrip(&Response::LinkResult {
593 exit_code: 0,
594 stdout: Arc::new(vec![]),
595 stderr: Arc::new(b"some warning".to_vec()),
596 cached: false,
597 warning: Some("non-deterministic: missing D flag".into()),
598 });
599 }
600
601 #[test]
602 fn session_stats_request_roundtrip() {
603 roundtrip(&Request::SessionStats {
604 session_id: "550e8400-e29b-41d4-a716-446655440000".into(),
605 });
606 }
607
608 #[test]
609 fn session_stats_result_roundtrip() {
610 let stats = SessionStats {
611 duration_ms: 5000,
612 compilations: 10,
613 hits: 7,
614 misses: 2,
615 non_cacheable: 1,
616 errors: 0,
617 time_saved_ms: 3000,
618 unique_sources: 9,
619 bytes_read: 50_000,
620 bytes_written: 20_000,
621 };
622 roundtrip(&Response::SessionStatsResult { stats: Some(stats) });
623 roundtrip(&Response::SessionStatsResult { stats: None });
624 }
625
626 #[test]
627 fn existing_request_variants_still_work() {
628 roundtrip(&Request::Ping);
629 roundtrip(&Request::Shutdown);
630 roundtrip(&Request::Status);
631 roundtrip(&Request::SessionEnd {
632 session_id: "550e8400-e29b-41d4-a716-446655440000".into(),
633 });
634 roundtrip(&Request::Compile {
635 session_id: "550e8400-e29b-41d4-a716-446655440000".into(),
636 args: vec!["-c".into(), "foo.c".into()],
637 cwd: "/tmp".into(),
638 compiler: "/usr/bin/gcc".into(),
639 env: None,
640 stdin: Vec::new(),
641 });
642 }
643
644 #[test]
645 fn existing_response_variants_still_work() {
646 roundtrip(&Response::Pong);
647 roundtrip(&Response::ShuttingDown);
648 roundtrip(&Response::CompileResult {
649 exit_code: 0,
650 stdout: Arc::new(vec![]),
651 stderr: Arc::new(vec![]),
652 cached: true,
653 });
654 roundtrip(&Response::Error {
655 message: "test".into(),
656 });
657 }
658
659 #[test]
660 fn daemon_status_version_field_roundtrips() {
661 let with_version = DaemonStatus {
662 version: "1.2.3".to_string(),
663 artifact_count: 0,
664 cache_size_bytes: 0,
665 metadata_entries: 0,
666 uptime_secs: 0,
667 cache_hits: 0,
668 cache_misses: 0,
669 total_compilations: 0,
670 non_cacheable: 0,
671 compile_errors: 0,
672 time_saved_ms: 0,
673 total_links: 0,
674 link_hits: 0,
675 link_misses: 0,
676 link_non_cacheable: 0,
677 dep_graph_contexts: 0,
678 dep_graph_files: 0,
679 sessions_total: 0,
680 sessions_active: 0,
681 cache_dir: "".into(),
682 dep_graph_version: 0,
683 dep_graph_disk_size: 0,
684 dep_graph_persisted: false,
685 };
686 roundtrip(&with_version);
687 }
688
689 const _: () = assert!(crate::PROTOCOL_VERSION > 0);
691 const _FINGERPRINT_VERSION: () = assert!(crate::PROTOCOL_VERSION == 8);
694
695 #[test]
696 fn fingerprint_check_roundtrip() {
697 roundtrip(&Request::FingerprintCheck {
698 cache_file: "/tmp/lint.json".into(),
699 cache_type: "two-layer".into(),
700 root: "/home/user/project/src".into(),
701 extensions: vec!["rs".into(), "toml".into()],
702 include_globs: vec![],
703 exclude: vec![".git".into(), "target".into()],
704 });
705 roundtrip(&Request::FingerprintCheck {
706 cache_file: "cache.json".into(),
707 cache_type: "hash".into(),
708 root: ".".into(),
709 extensions: vec![],
710 include_globs: vec!["**/*.cpp".into(), "**/*.h".into()],
711 exclude: vec![],
712 });
713 }
714
715 #[test]
716 fn fingerprint_mark_success_roundtrip() {
717 roundtrip(&Request::FingerprintMarkSuccess {
718 cache_file: "/tmp/lint.json".into(),
719 });
720 }
721
722 #[test]
723 fn fingerprint_mark_failure_roundtrip() {
724 roundtrip(&Request::FingerprintMarkFailure {
725 cache_file: "/tmp/lint.json".into(),
726 });
727 }
728
729 #[test]
730 fn fingerprint_invalidate_roundtrip() {
731 roundtrip(&Request::FingerprintInvalidate {
732 cache_file: "/tmp/lint.json".into(),
733 });
734 }
735
736 #[test]
737 fn fingerprint_check_result_roundtrip() {
738 roundtrip(&Response::FingerprintCheckResult {
739 decision: "skip".into(),
740 reason: None,
741 changed_files: vec![],
742 });
743 roundtrip(&Response::FingerprintCheckResult {
744 decision: "run".into(),
745 reason: Some("content changed".into()),
746 changed_files: vec!["src/main.rs".into(), "src/lib.rs".into()],
747 });
748 roundtrip(&Response::FingerprintCheckResult {
749 decision: "run".into(),
750 reason: Some("no cache file".into()),
751 changed_files: vec![],
752 });
753 }
754
755 #[test]
756 fn fingerprint_ack_roundtrip() {
757 roundtrip(&Response::FingerprintAck);
758 }
759
760 #[test]
761 fn list_rust_artifacts_request_roundtrip() {
762 roundtrip(&Request::ListRustArtifacts);
763 }
764
765 #[test]
766 fn rust_artifact_list_response_roundtrip() {
767 roundtrip(&Response::RustArtifactList {
768 artifacts: vec![
769 RustArtifactInfo {
770 cache_key: "abc123def456".into(),
771 output_names: vec![
772 "libfoo-abc123.rlib".into(),
773 "libfoo-abc123.rmeta".into(),
774 "foo-abc123.d".into(),
775 ],
776 payload_count: 3,
777 },
778 RustArtifactInfo {
779 cache_key: "deadbeef".into(),
780 output_names: vec!["libbar-deadbeef.rlib".into()],
781 payload_count: 1,
782 },
783 ],
784 });
785 roundtrip(&Response::RustArtifactList { artifacts: vec![] });
787 }
788
789 #[test]
790 fn rust_artifact_info_roundtrip() {
791 roundtrip(&RustArtifactInfo {
792 cache_key: "0123456789abcdef".into(),
793 output_names: vec!["test.o".into()],
794 payload_count: 1,
795 });
796 }
797
798 #[test]
799 fn artifact_clone_shares_payload_via_arc() {
800 let artifact = ArtifactData {
801 outputs: vec![ArtifactOutput {
802 name: "test.o".into(),
803 data: Arc::new(vec![1, 2, 3, 4]),
804 }],
805 stdout: Arc::new(vec![5, 6]),
806 stderr: Arc::new(vec![7, 8]),
807 exit_code: 0,
808 };
809
810 let cloned = artifact.clone();
811
812 assert!(Arc::ptr_eq(
814 &artifact.outputs[0].data,
815 &cloned.outputs[0].data
816 ));
817 assert!(Arc::ptr_eq(&artifact.stdout, &cloned.stdout));
818 assert!(Arc::ptr_eq(&artifact.stderr, &cloned.stderr));
819 }
820
821 #[test]
822 fn arc_vec_u8_roundtrip_matches_plain_vec() {
823 let plain: Vec<u8> = vec![0xDE, 0xAD, 0xBE, 0xEF];
825 let arc_wrapped: Arc<Vec<u8>> = Arc::new(plain.clone());
826
827 let plain_bytes = bincode::serialize(&plain).unwrap();
828 let arc_bytes = bincode::serialize(&arc_wrapped).unwrap();
829 assert_eq!(
830 plain_bytes, arc_bytes,
831 "Arc<Vec<u8>> must serialize identically to Vec<u8>"
832 );
833
834 let decoded_plain: Vec<u8> = bincode::deserialize(&arc_bytes).unwrap();
836 let decoded_arc: Arc<Vec<u8>> = bincode::deserialize(&plain_bytes).unwrap();
837 assert_eq!(decoded_plain, plain);
838 assert_eq!(*decoded_arc, plain);
839 }
840}