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 profile: bool,
46 },
47 Compile {
49 session_id: String,
51 args: Vec<String>,
53 cwd: NormalizedPath,
55 compiler: NormalizedPath,
57 env: Option<Vec<(String, String)>>,
61 stdin: Vec<u8>,
67 },
68 SessionEnd {
70 session_id: String,
72 },
73 Clear,
75 CompileEphemeral {
79 client_pid: u32,
81 working_dir: NormalizedPath,
83 compiler: NormalizedPath,
85 args: Vec<String>,
87 cwd: NormalizedPath,
89 env: Option<Vec<(String, String)>>,
91 stdin: Vec<u8>,
95 },
96 LinkEphemeral {
99 client_pid: u32,
101 tool: NormalizedPath,
103 args: Vec<String>,
105 cwd: NormalizedPath,
107 env: Option<Vec<(String, String)>>,
109 },
110 SessionStats {
113 session_id: String,
115 },
116 FingerprintCheck {
119 cache_file: NormalizedPath,
121 cache_type: String,
123 root: NormalizedPath,
125 extensions: Vec<String>,
128 include_globs: Vec<String>,
131 exclude: Vec<String>,
133 },
134 FingerprintMarkSuccess {
136 cache_file: NormalizedPath,
138 },
139 FingerprintMarkFailure {
141 cache_file: NormalizedPath,
143 },
144 FingerprintInvalidate {
146 cache_file: NormalizedPath,
148 },
149 ListRustArtifacts,
152}
153
154#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
156pub enum Response {
157 Pong,
159 ShuttingDown,
161 Status(DaemonStatus),
163 LookupResult(LookupResult),
165 StoreResult(StoreResult),
167 SessionStarted {
169 session_id: String,
171 journal_path: Option<NormalizedPath>,
173 },
174 CompileResult {
176 exit_code: i32,
178 stdout: Arc<Vec<u8>>,
180 stderr: Arc<Vec<u8>>,
182 cached: bool,
184 },
185 SessionEnded {
187 stats: Option<SessionStats>,
189 },
190 LinkResult {
192 exit_code: i32,
194 stdout: Arc<Vec<u8>>,
196 stderr: Arc<Vec<u8>>,
198 cached: bool,
200 warning: Option<String>,
202 },
203 Error {
205 message: String,
207 },
208 Cleared {
210 artifacts_removed: u64,
212 metadata_cleared: u64,
214 dep_graph_contexts_cleared: u64,
216 on_disk_bytes_freed: u64,
218 },
219 SessionStatsResult {
222 stats: Option<SessionStats>,
224 },
225 FingerprintCheckResult {
228 decision: String,
230 reason: Option<String>,
232 changed_files: Vec<String>,
234 },
235 FingerprintAck,
237 RustArtifactList { artifacts: Vec<RustArtifactInfo> },
240}
241
242#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
244pub struct DaemonStatus {
245 pub version: String,
247 pub artifact_count: u64,
249 pub cache_size_bytes: u64,
251 pub metadata_entries: u64,
253 pub uptime_secs: u64,
255 pub cache_hits: u64,
257 pub cache_misses: u64,
259 pub total_compilations: u64,
261 pub non_cacheable: u64,
263 pub compile_errors: u64,
265 pub time_saved_ms: u64,
267 pub total_links: u64,
269 pub link_hits: u64,
271 pub link_misses: u64,
273 pub link_non_cacheable: u64,
275 pub dep_graph_contexts: u64,
277 pub dep_graph_files: u64,
279 pub sessions_total: u64,
281 pub sessions_active: u64,
283 pub cache_dir: NormalizedPath,
285 pub dep_graph_version: u32,
287 pub dep_graph_disk_size: u64,
289 pub dep_graph_persisted: bool,
295}
296
297#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
299pub enum LookupResult {
300 Hit {
302 artifact: ArtifactData,
304 },
305 Miss,
307}
308
309#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
311pub enum StoreResult {
312 Stored,
314 AlreadyExists,
316}
317
318#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
320pub struct ArtifactData {
321 pub outputs: Vec<ArtifactOutput>,
323 pub stdout: Arc<Vec<u8>>,
325 pub stderr: Arc<Vec<u8>>,
327 pub exit_code: i32,
329}
330
331#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
333pub struct SessionStats {
334 pub duration_ms: u64,
336 pub compilations: u64,
338 pub hits: u64,
340 pub misses: u64,
342 pub non_cacheable: u64,
344 pub errors: u64,
346 pub time_saved_ms: u64,
348 pub unique_sources: u64,
350 pub bytes_read: u64,
352 pub bytes_written: u64,
354}
355
356#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
369pub enum ArtifactPayload {
370 Bytes(Arc<Vec<u8>>),
373 Path(NormalizedPath),
378}
379
380impl ArtifactPayload {
381 #[must_use]
385 pub fn size_bytes(&self) -> u64 {
386 match self {
387 Self::Bytes(b) => b.len() as u64,
388 Self::Path(p) => std::fs::metadata(p.as_path()).map(|m| m.len()).unwrap_or(0),
389 }
390 }
391
392 #[must_use]
397 pub fn as_bytes(&self) -> Option<&Arc<Vec<u8>>> {
398 match self {
399 Self::Bytes(b) => Some(b),
400 Self::Path(_) => None,
401 }
402 }
403}
404
405#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
407pub struct ArtifactOutput {
408 pub name: String,
410 pub payload: ArtifactPayload,
413}
414
415#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
417pub struct RustArtifactInfo {
418 pub cache_key: String,
420 pub output_names: Vec<String>,
422 pub payload_count: usize,
424}
425
426#[cfg(test)]
427mod tests {
428 use super::*;
429
430 fn roundtrip<T: Serialize + serde::de::DeserializeOwned + PartialEq + std::fmt::Debug>(
432 val: &T,
433 ) {
434 let bytes = bincode::serialize(val).unwrap();
435 let decoded: T = bincode::deserialize(&bytes).unwrap();
436 assert_eq!(*val, decoded);
437 }
438
439 #[test]
440 fn session_stats_roundtrip() {
441 let stats = SessionStats {
442 duration_ms: 12345,
443 compilations: 100,
444 hits: 80,
445 misses: 15,
446 non_cacheable: 5,
447 errors: 2,
448 time_saved_ms: 8000,
449 unique_sources: 42,
450 bytes_read: 1024 * 1024,
451 bytes_written: 512 * 1024,
452 };
453 roundtrip(&stats);
454 }
455
456 #[test]
457 fn session_stats_default_zeros() {
458 let stats = SessionStats {
459 duration_ms: 0,
460 compilations: 0,
461 hits: 0,
462 misses: 0,
463 non_cacheable: 0,
464 errors: 0,
465 time_saved_ms: 0,
466 unique_sources: 0,
467 bytes_read: 0,
468 bytes_written: 0,
469 };
470 roundtrip(&stats);
471 }
472
473 #[test]
474 fn daemon_status_expanded_roundtrip() {
475 let status = DaemonStatus {
476 version: env!("CARGO_PKG_VERSION").to_string(),
477 artifact_count: 892,
478 cache_size_bytes: 147_000_000,
479 metadata_entries: 5430,
480 uptime_secs: 8040,
481 cache_hits: 1089,
482 cache_misses: 143,
483 total_compilations: 1247,
484 non_cacheable: 15,
485 compile_errors: 3,
486 time_saved_ms: 750_000,
487 total_links: 50,
488 link_hits: 38,
489 link_misses: 10,
490 link_non_cacheable: 2,
491 dep_graph_contexts: 892,
492 dep_graph_files: 4201,
493 sessions_total: 41,
494 sessions_active: 3,
495 cache_dir: "/home/user/.zccache".into(),
496 dep_graph_version: 1,
497 dep_graph_disk_size: 2_500_000,
498 dep_graph_persisted: true,
499 };
500 roundtrip(&status);
501 }
502
503 #[test]
504 fn session_start_with_track_stats_roundtrip() {
505 let req = Request::SessionStart {
506 client_pid: 1234,
507 working_dir: "/home/user/project".into(),
508 log_file: None,
509 track_stats: true,
510 journal_path: None,
511 profile: false,
512 };
513 roundtrip(&req);
514
515 let req_no_stats = Request::SessionStart {
516 client_pid: 1234,
517 working_dir: "/home/user/project".into(),
518 log_file: None,
519 track_stats: false,
520 journal_path: None,
521 profile: false,
522 };
523 roundtrip(&req_no_stats);
524 }
525
526 #[test]
527 fn session_start_with_journal_path_roundtrip() {
528 let req = Request::SessionStart {
529 client_pid: 5678,
530 working_dir: "/home/user/project".into(),
531 log_file: None,
532 track_stats: false,
533 journal_path: Some("/tmp/build.jsonl".into()),
534 profile: false,
535 };
536 roundtrip(&req);
537
538 let req_no_journal = Request::SessionStart {
539 client_pid: 5678,
540 working_dir: "/home/user/project".into(),
541 log_file: None,
542 track_stats: false,
543 journal_path: None,
544 profile: false,
545 };
546 roundtrip(&req_no_journal);
547 }
548
549 #[test]
550 fn session_started_with_journal_path_roundtrip() {
551 let resp = Response::SessionStarted {
552 session_id: "550e8400-e29b-41d4-a716-446655440000".into(),
553 journal_path: Some("/home/user/.zccache/logs/sessions/test.jsonl".into()),
554 };
555 roundtrip(&resp);
556
557 let resp_no_journal = Response::SessionStarted {
558 session_id: "550e8400-e29b-41d4-a716-446655440000".into(),
559 journal_path: None,
560 };
561 roundtrip(&resp_no_journal);
562 }
563
564 #[test]
565 fn session_ended_with_stats_roundtrip() {
566 let stats = SessionStats {
567 duration_ms: 34000,
568 compilations: 32,
569 hits: 28,
570 misses: 3,
571 non_cacheable: 1,
572 errors: 0,
573 time_saved_ms: 8200,
574 unique_sources: 30,
575 bytes_read: 2_000_000,
576 bytes_written: 500_000,
577 };
578 let resp = Response::SessionEnded { stats: Some(stats) };
579 roundtrip(&resp);
580
581 let resp_no_stats = Response::SessionEnded { stats: None };
582 roundtrip(&resp_no_stats);
583 }
584
585 #[test]
586 fn clear_request_roundtrip() {
587 roundtrip(&Request::Clear);
588 }
589
590 #[test]
591 fn cleared_response_roundtrip() {
592 roundtrip(&Response::Cleared {
593 artifacts_removed: 42,
594 metadata_cleared: 100,
595 dep_graph_contexts_cleared: 25,
596 on_disk_bytes_freed: 1024 * 1024,
597 });
598 }
599
600 #[test]
601 fn compile_ephemeral_roundtrip() {
602 roundtrip(&Request::CompileEphemeral {
603 client_pid: 9876,
604 working_dir: "/home/user/project".into(),
605 compiler: "/usr/bin/clang++".into(),
606 args: vec!["-c".into(), "main.cpp".into(), "-o".into(), "main.o".into()],
607 cwd: "/home/user/project/build".into(),
608 env: Some(vec![("PATH".into(), "/usr/bin".into())]),
609 stdin: Vec::new(),
610 });
611 roundtrip(&Request::CompileEphemeral {
615 client_pid: 1,
616 working_dir: ".".into(),
617 compiler: "gcc".into(),
618 args: vec![],
619 cwd: ".".into(),
620 env: None,
621 stdin: b"hello\x00world\nbinary\xff\xfe".to_vec(),
622 });
623 }
624
625 #[test]
626 fn link_ephemeral_roundtrip() {
627 roundtrip(&Request::LinkEphemeral {
628 client_pid: 5555,
629 tool: "/usr/bin/ar".into(),
630 args: vec!["rcs".into(), "libfoo.a".into(), "a.o".into(), "b.o".into()],
631 cwd: "/home/user/project/build".into(),
632 env: Some(vec![("PATH".into(), "/usr/bin".into())]),
633 });
634 roundtrip(&Request::LinkEphemeral {
635 client_pid: 1,
636 tool: "lib.exe".into(),
637 args: vec!["/OUT:foo.lib".into(), "a.obj".into()],
638 cwd: ".".into(),
639 env: None,
640 });
641 }
642
643 #[test]
644 fn link_result_roundtrip() {
645 roundtrip(&Response::LinkResult {
646 exit_code: 0,
647 stdout: Arc::new(vec![]),
648 stderr: Arc::new(vec![]),
649 cached: true,
650 warning: None,
651 });
652 roundtrip(&Response::LinkResult {
653 exit_code: 0,
654 stdout: Arc::new(vec![]),
655 stderr: Arc::new(b"some warning".to_vec()),
656 cached: false,
657 warning: Some("non-deterministic: missing D flag".into()),
658 });
659 }
660
661 #[test]
662 fn session_stats_request_roundtrip() {
663 roundtrip(&Request::SessionStats {
664 session_id: "550e8400-e29b-41d4-a716-446655440000".into(),
665 });
666 }
667
668 #[test]
669 fn session_stats_result_roundtrip() {
670 let stats = SessionStats {
671 duration_ms: 5000,
672 compilations: 10,
673 hits: 7,
674 misses: 2,
675 non_cacheable: 1,
676 errors: 0,
677 time_saved_ms: 3000,
678 unique_sources: 9,
679 bytes_read: 50_000,
680 bytes_written: 20_000,
681 };
682 roundtrip(&Response::SessionStatsResult { stats: Some(stats) });
683 roundtrip(&Response::SessionStatsResult { stats: None });
684 }
685
686 #[test]
687 fn existing_request_variants_still_work() {
688 roundtrip(&Request::Ping);
689 roundtrip(&Request::Shutdown);
690 roundtrip(&Request::Status);
691 roundtrip(&Request::SessionEnd {
692 session_id: "550e8400-e29b-41d4-a716-446655440000".into(),
693 });
694 roundtrip(&Request::Compile {
695 session_id: "550e8400-e29b-41d4-a716-446655440000".into(),
696 args: vec!["-c".into(), "foo.c".into()],
697 cwd: "/tmp".into(),
698 compiler: "/usr/bin/gcc".into(),
699 env: None,
700 stdin: Vec::new(),
701 });
702 }
703
704 #[test]
705 fn existing_response_variants_still_work() {
706 roundtrip(&Response::Pong);
707 roundtrip(&Response::ShuttingDown);
708 roundtrip(&Response::CompileResult {
709 exit_code: 0,
710 stdout: Arc::new(vec![]),
711 stderr: Arc::new(vec![]),
712 cached: true,
713 });
714 roundtrip(&Response::Error {
715 message: "test".into(),
716 });
717 }
718
719 #[test]
720 fn daemon_status_version_field_roundtrips() {
721 let with_version = DaemonStatus {
722 version: "1.2.3".to_string(),
723 artifact_count: 0,
724 cache_size_bytes: 0,
725 metadata_entries: 0,
726 uptime_secs: 0,
727 cache_hits: 0,
728 cache_misses: 0,
729 total_compilations: 0,
730 non_cacheable: 0,
731 compile_errors: 0,
732 time_saved_ms: 0,
733 total_links: 0,
734 link_hits: 0,
735 link_misses: 0,
736 link_non_cacheable: 0,
737 dep_graph_contexts: 0,
738 dep_graph_files: 0,
739 sessions_total: 0,
740 sessions_active: 0,
741 cache_dir: "".into(),
742 dep_graph_version: 0,
743 dep_graph_disk_size: 0,
744 dep_graph_persisted: false,
745 };
746 roundtrip(&with_version);
747 }
748
749 const _: () = assert!(crate::PROTOCOL_VERSION > 0);
751 const _FINGERPRINT_VERSION: () = assert!(crate::PROTOCOL_VERSION == 8);
755
756 #[test]
757 fn fingerprint_check_roundtrip() {
758 roundtrip(&Request::FingerprintCheck {
759 cache_file: "/tmp/lint.json".into(),
760 cache_type: "two-layer".into(),
761 root: "/home/user/project/src".into(),
762 extensions: vec!["rs".into(), "toml".into()],
763 include_globs: vec![],
764 exclude: vec![".git".into(), "target".into()],
765 });
766 roundtrip(&Request::FingerprintCheck {
767 cache_file: "cache.json".into(),
768 cache_type: "hash".into(),
769 root: ".".into(),
770 extensions: vec![],
771 include_globs: vec!["**/*.cpp".into(), "**/*.h".into()],
772 exclude: vec![],
773 });
774 }
775
776 #[test]
777 fn fingerprint_mark_success_roundtrip() {
778 roundtrip(&Request::FingerprintMarkSuccess {
779 cache_file: "/tmp/lint.json".into(),
780 });
781 }
782
783 #[test]
784 fn fingerprint_mark_failure_roundtrip() {
785 roundtrip(&Request::FingerprintMarkFailure {
786 cache_file: "/tmp/lint.json".into(),
787 });
788 }
789
790 #[test]
791 fn fingerprint_invalidate_roundtrip() {
792 roundtrip(&Request::FingerprintInvalidate {
793 cache_file: "/tmp/lint.json".into(),
794 });
795 }
796
797 #[test]
798 fn fingerprint_check_result_roundtrip() {
799 roundtrip(&Response::FingerprintCheckResult {
800 decision: "skip".into(),
801 reason: None,
802 changed_files: vec![],
803 });
804 roundtrip(&Response::FingerprintCheckResult {
805 decision: "run".into(),
806 reason: Some("content changed".into()),
807 changed_files: vec!["src/main.rs".into(), "src/lib.rs".into()],
808 });
809 roundtrip(&Response::FingerprintCheckResult {
810 decision: "run".into(),
811 reason: Some("no cache file".into()),
812 changed_files: vec![],
813 });
814 }
815
816 #[test]
817 fn fingerprint_ack_roundtrip() {
818 roundtrip(&Response::FingerprintAck);
819 }
820
821 #[test]
822 fn list_rust_artifacts_request_roundtrip() {
823 roundtrip(&Request::ListRustArtifacts);
824 }
825
826 #[test]
827 fn rust_artifact_list_response_roundtrip() {
828 roundtrip(&Response::RustArtifactList {
829 artifacts: vec![
830 RustArtifactInfo {
831 cache_key: "abc123def456".into(),
832 output_names: vec![
833 "libfoo-abc123.rlib".into(),
834 "libfoo-abc123.rmeta".into(),
835 "foo-abc123.d".into(),
836 ],
837 payload_count: 3,
838 },
839 RustArtifactInfo {
840 cache_key: "deadbeef".into(),
841 output_names: vec!["libbar-deadbeef.rlib".into()],
842 payload_count: 1,
843 },
844 ],
845 });
846 roundtrip(&Response::RustArtifactList { artifacts: vec![] });
848 }
849
850 #[test]
851 fn rust_artifact_info_roundtrip() {
852 roundtrip(&RustArtifactInfo {
853 cache_key: "0123456789abcdef".into(),
854 output_names: vec!["test.o".into()],
855 payload_count: 1,
856 });
857 }
858
859 #[test]
860 fn artifact_clone_shares_payload_via_arc() {
861 let bytes = Arc::new(vec![1u8, 2, 3, 4]);
862 let artifact = ArtifactData {
863 outputs: vec![ArtifactOutput {
864 name: "test.o".into(),
865 payload: ArtifactPayload::Bytes(Arc::clone(&bytes)),
866 }],
867 stdout: Arc::new(vec![5, 6]),
868 stderr: Arc::new(vec![7, 8]),
869 exit_code: 0,
870 };
871
872 let cloned = artifact.clone();
873
874 let orig_inner = artifact.outputs[0].payload.as_bytes().unwrap();
876 let cloned_inner = cloned.outputs[0].payload.as_bytes().unwrap();
877 assert!(Arc::ptr_eq(orig_inner, cloned_inner));
878 assert!(Arc::ptr_eq(orig_inner, &bytes));
879 assert!(Arc::ptr_eq(&artifact.stdout, &cloned.stdout));
880 assert!(Arc::ptr_eq(&artifact.stderr, &cloned.stderr));
881 }
882
883 #[test]
884 fn artifact_payload_size_bytes_for_bytes_variant() {
885 let p = ArtifactPayload::Bytes(Arc::new(vec![0u8; 1234]));
886 assert_eq!(p.size_bytes(), 1234);
887 }
888
889 #[test]
890 fn artifact_payload_size_bytes_for_path_variant() {
891 let tmp = tempfile::NamedTempFile::new().expect("tempfile");
892 std::fs::write(tmp.path(), vec![0u8; 4321]).expect("write");
893 let p = ArtifactPayload::Path(NormalizedPath::from(tmp.path()));
894 assert_eq!(p.size_bytes(), 4321);
895 }
896
897 #[test]
898 fn artifact_payload_size_bytes_for_missing_path_is_zero() {
899 let p = ArtifactPayload::Path(NormalizedPath::from(std::path::Path::new(
900 "/this/path/does/not/exist/zccache",
901 )));
902 assert_eq!(p.size_bytes(), 0);
903 }
904
905 #[test]
906 fn artifact_payload_round_trips_through_bincode() {
907 let bytes_variant = ArtifactPayload::Bytes(Arc::new(b"hello".to_vec()));
908 let encoded = bincode::serialize(&bytes_variant).expect("serialize bytes");
909 let decoded: ArtifactPayload = bincode::deserialize(&encoded).expect("deserialize bytes");
910 assert_eq!(decoded, bytes_variant);
911
912 let path_variant = ArtifactPayload::Path(NormalizedPath::from(std::path::Path::new(
913 "/tmp/some/place.rlib",
914 )));
915 let encoded = bincode::serialize(&path_variant).expect("serialize path");
916 let decoded: ArtifactPayload = bincode::deserialize(&encoded).expect("deserialize path");
917 assert_eq!(decoded, path_variant);
918 }
919
920 #[test]
921 fn arc_vec_u8_roundtrip_matches_plain_vec() {
922 let plain: Vec<u8> = vec![0xDE, 0xAD, 0xBE, 0xEF];
924 let arc_wrapped: Arc<Vec<u8>> = Arc::new(plain.clone());
925
926 let plain_bytes = bincode::serialize(&plain).unwrap();
927 let arc_bytes = bincode::serialize(&arc_wrapped).unwrap();
928 assert_eq!(
929 plain_bytes, arc_bytes,
930 "Arc<Vec<u8>> must serialize identically to Vec<u8>"
931 );
932
933 let decoded_plain: Vec<u8> = bincode::deserialize(&arc_bytes).unwrap();
935 let decoded_arc: Arc<Vec<u8>> = bincode::deserialize(&plain_bytes).unwrap();
936 assert_eq!(decoded_plain, plain);
937 assert_eq!(*decoded_arc, plain);
938 }
939}