1use std::ffi::{OsStr, OsString};
4use std::path::{Path, PathBuf};
5use std::process::{Command, Stdio};
6use std::time::{Duration, Instant};
7
8use code_system_graph_core::{
9 CodeGraphConfig, CodeGraphProvider, ConfigSource, EffectiveRepositoryConfig, IgnorePolicy, ProviderBudget, ProviderRequest, ProviderStatus
10};
11use code_system_graph_model::RepoId;
12use schemars::JsonSchema;
13use serde::{Deserialize, Serialize};
14use tokio_util::sync::CancellationToken;
15
16use super::{
17 ApplicationError, ScanOverrides, ScanSummary, load_workspace_context, work_database_instance_id
18};
19
20const MAX_PERSISTED_WATCH_TARGET_BYTES: u64 = 8 * 1024 * 1024;
21
22#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct SyncTarget {
25 pub alias: String,
27 pub path: PathBuf,
29 pub ignore_policy: IgnorePolicy,
31 pub explicit_paths: Vec<PathBuf>,
33}
34
35#[derive(Debug, Serialize, Deserialize)]
36struct PersistedWatchTarget {
37 alias: String,
38 path: PathBuf,
39 configured_excludes: Vec<String>,
40 configured_excludes_source: ConfigSource,
41 include_defaults: Vec<String>,
42 include_defaults_source: ConfigSource,
43 explicit_paths: Vec<PathBuf>,
44}
45
46impl From<&SyncTarget> for PersistedWatchTarget {
47 fn from(target: &SyncTarget) -> Self {
48 Self {
49 alias: target.alias.clone(),
50 path: target.path.clone(),
51 configured_excludes: target.ignore_policy.configured_excludes().to_vec(),
52 configured_excludes_source: target.ignore_policy.configured_excludes_source(),
53 include_defaults: target.ignore_policy.include_defaults().to_vec(),
54 include_defaults_source: target.ignore_policy.include_defaults_source(),
55 explicit_paths: target.explicit_paths.clone(),
56 }
57 }
58}
59
60impl TryFrom<PersistedWatchTarget> for SyncTarget {
61 type Error = ApplicationError;
62
63 fn try_from(target: PersistedWatchTarget) -> Result<Self, Self::Error> {
64 let ignore_policy = IgnorePolicy::new(
65 target.configured_excludes,
66 target.configured_excludes_source,
67 target.include_defaults,
68 target.include_defaults_source,
69 )
70 .map_err(|error| {
71 ApplicationError::Initialization(format!("invalid persisted watch scope: {error}"))
72 })?;
73 Ok(Self {
74 alias: target.alias,
75 path: target.path,
76 ignore_policy,
77 explicit_paths: target.explicit_paths,
78 })
79 }
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
84#[serde(rename_all = "snake_case")]
85pub enum CodeGraphSyncState {
86 Synchronized,
88 SkippedNotInitialized,
90 Failed,
92}
93
94#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
96pub struct CodeGraphRepositorySync {
97 pub repository: String,
99 pub state: CodeGraphSyncState,
101 pub detail: Option<String>,
103}
104
105#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
107pub struct CodeGraphSyncSummary {
108 pub enabled: bool,
110 pub repository_count: usize,
112 pub synchronized_count: usize,
114 #[serde(default)]
116 pub changed_count: usize,
117 #[serde(default)]
119 pub unchanged_count: usize,
120 pub skipped_count: usize,
122 pub failed_count: usize,
124 pub repositories: Vec<CodeGraphRepositorySync>,
126}
127
128#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
130pub struct SyncSummary {
131 pub schema_version: u8,
133 pub execution: code_system_graph_core::ExecutionSummary,
135 pub scan: ScanSummary,
137 pub codegraph: CodeGraphSyncSummary,
139}
140
141pub fn workspace_sync_targets(
148 config_path: &Path,
149 overrides: &ScanOverrides,
150) -> Result<Vec<SyncTarget>, ApplicationError> {
151 let context = load_workspace_context(config_path, overrides)?;
152 if let Some(requested) = &overrides.workspace
153 && requested != &context.manifest.name
154 {
155 return Err(ApplicationError::WorkspaceNameMismatch {
156 requested: requested.clone(),
157 manifest: context.manifest.name,
158 });
159 }
160 if let Some(selected) = &overrides.repository
161 && !context
162 .registry
163 .record
164 .repositories
165 .iter()
166 .any(|repository| &repository.alias == selected)
167 {
168 return Err(ApplicationError::UnknownOverrideRepository(
169 selected.clone(),
170 ));
171 }
172
173 context
174 .registry
175 .record
176 .repositories
177 .iter()
178 .filter(|repository| {
179 overrides
180 .repository
181 .as_ref()
182 .is_none_or(|selected| selected == &repository.alias)
183 })
184 .map(|repository| {
185 let path = context
186 .registry
187 .checkout_path(&repository.alias)
188 .ok_or_else(|| ApplicationError::RegistryAliasMissing(repository.alias.clone()))?;
189 let effective = context
190 .repository_configs
191 .get(&repository.alias)
192 .ok_or_else(|| ApplicationError::RegistryAliasMissing(repository.alias.clone()))?;
193 Ok(SyncTarget {
194 alias: repository.alias.clone(),
195 path: path.to_path_buf(),
196 ignore_policy: effective.ignore_policy.clone(),
197 explicit_paths: explicit_watch_paths(effective),
198 })
199 })
200 .collect()
201}
202
203fn explicit_watch_paths(config: &EffectiveRepositoryConfig) -> Vec<PathBuf> {
204 let mut paths = vec![PathBuf::from(".code-system-graph.yaml")];
205 paths.extend(config.openapi.iter().map(PathBuf::from));
206 paths.extend(
207 config
208 .http_consumers
209 .iter()
210 .map(|consumer| PathBuf::from(&consumer.source)),
211 );
212 paths.extend(
213 config
214 .integration_tests
215 .iter()
216 .map(|test| PathBuf::from(&test.path)),
217 );
218 paths.extend(
219 config
220 .implementations
221 .iter()
222 .map(|implementation| PathBuf::from(&implementation.path)),
223 );
224 paths.sort();
225 paths.dedup();
226 paths
227}
228
229pub fn sync_workspace_with_overrides(
239 config_path: &Path,
240 database_path: &Path,
241 overrides: &ScanOverrides,
242 synchronize_codegraph: bool,
243) -> Result<SyncSummary, ApplicationError> {
244 super::worker::supervise_sync(config_path, database_path, overrides, synchronize_codegraph)
245}
246
247pub fn sync_workspace_with_worker_executable(
256 config_path: &Path,
257 database_path: &Path,
258 overrides: &ScanOverrides,
259 synchronize_codegraph: bool,
260 worker_executable: &Path,
261) -> Result<SyncSummary, ApplicationError> {
262 super::worker::supervise_sync_with_executable(
263 config_path,
264 database_path,
265 overrides,
266 synchronize_codegraph,
267 worker_executable,
268 )
269}
270
271#[doc(hidden)]
272pub fn sync_workspace_with_wall_time_cap(
273 config_path: &Path,
274 database_path: &Path,
275 overrides: &ScanOverrides,
276 synchronize_codegraph: bool,
277 wall_time_cap_ms: u64,
278) -> Result<SyncSummary, ApplicationError> {
279 super::worker::supervise_sync_with_wall_time_cap(
280 config_path,
281 database_path,
282 overrides,
283 synchronize_codegraph,
284 wall_time_cap_ms,
285 )
286}
287
288pub(crate) fn sync_workspace_direct(
289 config_path: &Path,
290 database_path: &Path,
291 overrides: &ScanOverrides,
292 synchronize_codegraph: bool,
293) -> Result<SyncSummary, ApplicationError> {
294 let context = load_workspace_context(config_path, overrides)?;
295 let policy = context.execution_policy;
296 let workspace = context.manifest.name;
297 let targets = workspace_sync_targets(config_path, overrides)?;
298 persist_watch_targets(database_path, &workspace, &targets)?;
299 let binary = codegraph_binary(overrides);
300 let codegraph_timeout = Duration::from_millis(policy.max_codegraph_sync_wall_time_ms_per_repo);
301 let codegraph = synchronize_codegraph_targets(
302 &targets,
303 synchronize_codegraph,
304 codegraph_timeout,
305 |target, deadline| run_codegraph_status(&binary, target, deadline),
306 |path, deadline| run_codegraph_sync(&binary, path, deadline),
307 );
308 let mut scan_overrides = overrides.clone();
309 scan_overrides.codegraph = codegraph.synchronized_count > 0;
310 let scan = super::scan_workspace_direct_for_sync(
311 config_path,
312 database_path,
313 &scan_overrides,
314 codegraph.changed_count == 0,
315 )?;
316 Ok(SyncSummary {
317 schema_version: 1,
318 execution: code_system_graph_core::ExecutionSummary::default(),
319 scan,
320 codegraph,
321 })
322}
323
324fn run_codegraph_status(
325 binary: &OsStr,
326 target: &SyncTarget,
327 deadline: Instant,
328) -> Result<ProviderStatus, String> {
329 let binary = binary.to_owned();
330 let alias = target.alias.clone();
331 let project_path = target.path.clone();
332 std::thread::spawn(move || {
333 let timeout = deadline.saturating_duration_since(Instant::now());
334 if timeout.is_zero() {
335 return Err(
336 "CodeGraph status exceeded the repository synchronization deadline".to_owned(),
337 );
338 }
339 let runtime = tokio::runtime::Builder::new_current_thread()
340 .enable_all()
341 .build()
342 .map_err(|error| format!("cannot start CodeGraph status runtime: {error}"))?;
343 let provider = CodeGraphProvider::new(CodeGraphConfig {
344 binary,
345 max_concurrent_processes: 1,
346 ..CodeGraphConfig::default()
347 })
348 .map_err(|error| format!("cannot configure CodeGraph status provider: {error}"))?;
349 runtime
350 .block_on(provider.index_status(ProviderRequest {
351 repo_id: RepoId::new(format!("sync:{alias}")),
352 project_path,
353 budget: ProviderBudget {
354 timeout,
355 max_output_bytes: 256 * 1024,
356 max_items: 1,
357 },
358 cancellation: CancellationToken::new(),
359 }))
360 .map_err(|error| format!("cannot inspect CodeGraph status: {error}"))
361 })
362 .join()
363 .map_err(|_| "CodeGraph status worker terminated unexpectedly".to_owned())?
364}
365
366fn persist_watch_targets(
367 database_path: &Path,
368 workspace: &str,
369 targets: &[SyncTarget],
370) -> Result<(), ApplicationError> {
371 let mut encoded = Vec::with_capacity(targets.len());
372 for target in targets {
373 let payload = serde_json::to_vec(&PersistedWatchTarget::from(target))
374 .map_err(|error| ApplicationError::Initialization(error.to_string()))?;
375 if u64::try_from(payload.len()).unwrap_or(u64::MAX) > MAX_PERSISTED_WATCH_TARGET_BYTES {
376 return Err(ApplicationError::Initialization(
377 "persisted watch target exceeded its protocol bound".to_owned(),
378 ));
379 }
380 encoded.push((target.alias.clone(), payload));
381 }
382 let database_instance_id =
383 code_system_graph_store_sqlite::SqliteStore::open(database_path)?.database_instance_id()?;
384 super::work_state::WorkState::open(database_path, &database_instance_id)
385 .and_then(|mut state| state.replace_watch_scope(workspace, &encoded))
386 .map_err(ApplicationError::Initialization)
387}
388
389#[doc(hidden)]
390pub fn load_persisted_watch_targets(
391 database_path: &Path,
392 workspace: &str,
393) -> Result<Vec<SyncTarget>, ApplicationError> {
394 let database_instance_id = work_database_instance_id(database_path)?;
395 let state = super::work_state::WorkState::open(database_path, &database_instance_id)
396 .map_err(ApplicationError::Initialization)?;
397 state
398 .load_watch_scope(workspace, MAX_PERSISTED_WATCH_TARGET_BYTES)
399 .map_err(ApplicationError::Initialization)?
400 .into_iter()
401 .map(|encoded| {
402 serde_json::from_slice::<PersistedWatchTarget>(&encoded)
403 .map_err(|error| ApplicationError::Initialization(error.to_string()))?
404 .try_into()
405 })
406 .collect()
407}
408
409fn codegraph_binary(overrides: &ScanOverrides) -> OsString {
410 overrides
411 .codegraph_binary
412 .as_ref()
413 .map(|path| path.as_os_str().to_owned())
414 .or_else(|| {
415 std::env::var_os("CODE_SYSTEM_GRAPH_CODEGRAPH_BINARY").filter(|value| !value.is_empty())
416 })
417 .unwrap_or_else(|| OsString::from("codegraph"))
418}
419
420fn run_codegraph_sync(
421 binary: &OsStr,
422 project_path: &Path,
423 deadline: Instant,
424) -> Result<(), String> {
425 if Instant::now() >= deadline {
426 return Err("codegraph sync exceeded the repository synchronization deadline".to_owned());
427 }
428 let mut command = Command::new(binary);
429 command
430 .arg("sync")
431 .arg("--quiet")
432 .arg(project_path)
433 .current_dir(project_path)
434 .stdin(Stdio::null())
435 .stdout(Stdio::null())
436 .stderr(Stdio::null());
437 configure_codegraph_process_group(&mut command);
438 let mut child = command
439 .spawn()
440 .map_err(|error| format!("failed to start codegraph sync: {error}"))?;
441 let status = loop {
442 if let Some(status) = child
443 .try_wait()
444 .map_err(|error| format!("failed to wait for codegraph sync: {error}"))?
445 {
446 break status;
447 }
448 if Instant::now() >= deadline {
449 super::worker::terminate_process_tree(child.id());
450 terminate_codegraph_process_group(&mut child);
451 return Err(
452 "codegraph sync exceeded the repository synchronization deadline and was terminated"
453 .to_owned(),
454 );
455 }
456 std::thread::sleep(Duration::from_millis(50));
457 };
458 cleanup_exited_codegraph_process_group(child.id());
459 if status.success() {
460 return Ok(());
461 }
462 Err(format!("codegraph sync exited with {status}"))
463}
464
465#[cfg(unix)]
466fn configure_codegraph_process_group(command: &mut Command) {
467 use std::os::unix::process::CommandExt;
468 command.process_group(0);
469}
470
471#[cfg(not(unix))]
472fn configure_codegraph_process_group(_command: &mut Command) {}
473
474#[cfg(unix)]
475fn terminate_codegraph_process_group(child: &mut std::process::Child) {
476 if let Ok(process_group_id) = i32::try_from(child.id()) {
477 let process_group_id = nix::unistd::Pid::from_raw(process_group_id);
478 let _ = nix::sys::signal::killpg(process_group_id, nix::sys::signal::Signal::SIGTERM);
479 std::thread::sleep(Duration::from_millis(100));
480 let _ = nix::sys::signal::killpg(process_group_id, nix::sys::signal::Signal::SIGKILL);
481 }
482 let _ = child.wait();
483}
484
485#[cfg(not(unix))]
486fn terminate_codegraph_process_group(child: &mut std::process::Child) {
487 let _ = child.kill();
488 let _ = child.wait();
489}
490
491#[cfg(unix)]
492fn cleanup_exited_codegraph_process_group(process_id: u32) {
493 if let Ok(process_group_id) = i32::try_from(process_id) {
494 let _ = nix::sys::signal::killpg(
495 nix::unistd::Pid::from_raw(process_group_id),
496 nix::sys::signal::Signal::SIGKILL,
497 );
498 }
499}
500
501#[cfg(not(unix))]
502fn cleanup_exited_codegraph_process_group(process_id: u32) {
503 super::worker::terminate_process_tree(process_id);
504}
505
506#[derive(Debug)]
507struct CodeGraphSyncObservation {
508 state: CodeGraphSyncState,
509 detail: Option<String>,
510 index_changed: bool,
511}
512
513fn synchronize_codegraph_targets<I, S>(
514 targets: &[SyncTarget],
515 enabled: bool,
516 timeout: Duration,
517 mut inspect: I,
518 mut synchronize: S,
519) -> CodeGraphSyncSummary
520where
521 I: FnMut(&SyncTarget, Instant) -> Result<ProviderStatus, String>,
522 S: FnMut(&Path, Instant) -> Result<(), String>,
523{
524 let mut observations = Vec::with_capacity(targets.len());
525 let mut repositories = Vec::with_capacity(targets.len());
526 if enabled {
527 for target in targets {
528 let deadline = Instant::now() + timeout;
529 let observation = if target.path.join(".codegraph").is_dir() {
530 match inspect(target, deadline) {
531 Ok(ProviderStatus::Available) => CodeGraphSyncObservation {
532 state: CodeGraphSyncState::Synchronized,
533 detail: None,
534 index_changed: false,
535 },
536 Ok(ProviderStatus::Stale) => match synchronize(&target.path, deadline) {
537 Ok(()) => CodeGraphSyncObservation {
538 state: CodeGraphSyncState::Synchronized,
539 detail: None,
540 index_changed: true,
541 },
542 Err(detail) => failed_sync_observation(&detail),
543 },
544 Ok(ProviderStatus::IndexMissing) => CodeGraphSyncObservation {
545 state: CodeGraphSyncState::SkippedNotInitialized,
546 detail: Some("local CodeGraph index is not initialized".to_owned()),
547 index_changed: false,
548 },
549 Ok(status) => failed_sync_observation(&format!(
550 "CodeGraph status is not usable for synchronization: {status:?}"
551 )),
552 Err(detail) => failed_sync_observation(&detail),
553 }
554 } else {
555 CodeGraphSyncObservation {
556 state: CodeGraphSyncState::SkippedNotInitialized,
557 detail: Some("local CodeGraph index is not initialized".to_owned()),
558 index_changed: false,
559 }
560 };
561 repositories.push(CodeGraphRepositorySync {
562 repository: target.alias.clone(),
563 state: observation.state,
564 detail: observation.detail.clone(),
565 });
566 observations.push(observation);
567 super::worker::report_progress(code_system_graph_core::JobPhase::CodeGraphSync, 1);
568 }
569 }
570 repositories.sort_by(|left, right| left.repository.cmp(&right.repository));
571 let synchronized_count = repositories
572 .iter()
573 .filter(|item| item.state == CodeGraphSyncState::Synchronized)
574 .count();
575 let changed_count = observations
576 .iter()
577 .filter(|item| item.state == CodeGraphSyncState::Synchronized && item.index_changed)
578 .count();
579 let unchanged_count = observations
580 .iter()
581 .filter(|item| item.state == CodeGraphSyncState::Synchronized && !item.index_changed)
582 .count();
583 let skipped_count = repositories
584 .iter()
585 .filter(|item| item.state == CodeGraphSyncState::SkippedNotInitialized)
586 .count();
587 let failed_count = repositories
588 .iter()
589 .filter(|item| item.state == CodeGraphSyncState::Failed)
590 .count();
591 CodeGraphSyncSummary {
592 enabled,
593 repository_count: targets.len(),
594 synchronized_count,
595 changed_count,
596 unchanged_count,
597 skipped_count,
598 failed_count,
599 repositories,
600 }
601}
602
603fn failed_sync_observation(detail: &str) -> CodeGraphSyncObservation {
604 CodeGraphSyncObservation {
605 state: CodeGraphSyncState::Failed,
606 detail: Some(bounded_detail(detail)),
607 index_changed: false,
608 }
609}
610
611fn bounded_detail(detail: &str) -> String {
612 const MAX_CHARS: usize = 512;
613 let normalized = detail.split_whitespace().collect::<Vec<_>>().join(" ");
614 if normalized.chars().count() <= MAX_CHARS {
615 return normalized;
616 }
617 let mut bounded = normalized.chars().take(MAX_CHARS).collect::<String>();
618 bounded.push_str("...");
619 bounded
620}
621
622#[cfg(test)]
623mod tests {
624 use std::cell::RefCell;
625
626 use super::*;
627
628 fn target(alias: &str, path: PathBuf) -> SyncTarget {
629 SyncTarget {
630 alias: alias.to_owned(),
631 path,
632 ignore_policy: IgnorePolicy::new(
633 Vec::new(),
634 code_system_graph_core::ConfigSource::Default,
635 Vec::new(),
636 code_system_graph_core::ConfigSource::Default,
637 )
638 .expect("built-in ignore policy"),
639 explicit_paths: Vec::new(),
640 }
641 }
642
643 #[test]
644 fn persisted_watch_scope_should_preserve_every_alias_policy() -> anyhow::Result<()> {
645 let temporary = tempfile::tempdir()?;
646 let database = temporary.path().join("graph.db");
647 let shared = temporary.path().join("shared");
648 std::fs::create_dir(&shared)?;
649 let mut restrictive = target("a-restrictive", shared.clone());
650 restrictive.ignore_policy = IgnorePolicy::new(
651 vec!["generated/**".to_owned()],
652 ConfigSource::WorkspaceManifest,
653 Vec::new(),
654 ConfigSource::Default,
655 )?;
656 let permissive = target("b-permissive", shared);
657 let expected = vec![restrictive, permissive];
658
659 persist_watch_targets(&database, "workspace", &expected)?;
660 let actual = load_persisted_watch_targets(&database, "workspace")?;
661
662 assert_eq!(actual, expected);
663 Ok(())
664 }
665
666 #[test]
667 fn synchronization_should_skip_uninitialized_indexes_and_bound_failures() -> anyhow::Result<()>
668 {
669 let temporary = tempfile::tempdir()?;
670 let initialized = temporary.path().join("initialized");
671 let absent = temporary.path().join("absent");
672 std::fs::create_dir_all(initialized.join(".codegraph"))?;
673 std::fs::create_dir(&absent)?;
674 let called = RefCell::new(Vec::new());
675 let targets = vec![target("zeta", initialized.clone()), target("alpha", absent)];
676
677 let report = synchronize_codegraph_targets(
678 &targets,
679 true,
680 Duration::from_secs(1),
681 |target, _| {
682 called.borrow_mut().push(target.path.clone());
683 Err("failure ".repeat(600))
684 },
685 |_, _| panic!("failed status inspection must not synchronize"),
686 );
687
688 assert_eq!(called.into_inner(), vec![initialized]);
689 assert_eq!(report.repository_count, 2);
690 assert_eq!(report.synchronized_count, 0);
691 assert_eq!(report.skipped_count, 1);
692 assert_eq!(report.failed_count, 1);
693 assert_eq!(report.repositories[0].repository, "alpha");
694 assert_eq!(
695 report.repositories[0].state,
696 CodeGraphSyncState::SkippedNotInitialized
697 );
698 assert!(
699 report.repositories[1]
700 .detail
701 .as_deref()
702 .is_some_and(|detail| detail.chars().count() <= 515)
703 );
704 Ok(())
705 }
706
707 #[test]
708 fn disabled_codegraph_sync_should_not_invoke_runner() {
709 let target = target("repo", PathBuf::from("repo"));
710 let report = synchronize_codegraph_targets(
711 &[target],
712 false,
713 Duration::from_secs(1),
714 |_, _| panic!("disabled synchronization must not inspect CodeGraph"),
715 |_, _| panic!("disabled synchronization must not invoke CodeGraph"),
716 );
717 assert!(!report.enabled);
718 assert_eq!(report.repository_count, 1);
719 assert!(report.repositories.is_empty());
720 }
721
722 #[test]
723 fn synchronization_should_distinguish_changed_and_current_indexes() -> anyhow::Result<()> {
724 let temporary = tempfile::tempdir()?;
725 let current = temporary.path().join("current");
726 let stale = temporary.path().join("stale");
727 std::fs::create_dir_all(current.join(".codegraph"))?;
728 std::fs::create_dir_all(stale.join(".codegraph"))?;
729 let synchronized = RefCell::new(Vec::new());
730 let targets = vec![target("current", current), target("stale", stale.clone())];
731
732 let report = synchronize_codegraph_targets(
733 &targets,
734 true,
735 Duration::from_secs(1),
736 |target, _| {
737 if target.alias == "stale" {
738 Ok(ProviderStatus::Stale)
739 } else {
740 Ok(ProviderStatus::Available)
741 }
742 },
743 |path, _| {
744 synchronized.borrow_mut().push(path.to_path_buf());
745 Ok(())
746 },
747 );
748
749 assert_eq!(synchronized.into_inner(), vec![stale]);
750 assert_eq!(report.synchronized_count, 2);
751 assert_eq!(report.changed_count, 1);
752 assert_eq!(report.unchanged_count, 1);
753 Ok(())
754 }
755
756 #[test]
757 fn synchronization_should_share_one_deadline_between_status_and_sync() -> anyhow::Result<()> {
758 let temporary = tempfile::tempdir()?;
759 let repository = temporary.path().join("stale");
760 std::fs::create_dir_all(repository.join(".codegraph"))?;
761 let inspected_deadline = RefCell::new(None);
762 let synchronized_deadline = RefCell::new(None);
763
764 let report = synchronize_codegraph_targets(
765 &[target("stale", repository)],
766 true,
767 Duration::from_secs(1),
768 |_, deadline| {
769 inspected_deadline.replace(Some(deadline));
770 Ok(ProviderStatus::Stale)
771 },
772 |_, deadline| {
773 synchronized_deadline.replace(Some(deadline));
774 Ok(())
775 },
776 );
777
778 assert_eq!(report.changed_count, 1);
779 assert_eq!(
780 inspected_deadline.into_inner(),
781 synchronized_deadline.into_inner()
782 );
783 Ok(())
784 }
785
786 #[cfg(unix)]
787 #[test]
788 fn codegraph_timeout_should_terminate_its_descendant_process() -> anyhow::Result<()> {
789 use std::os::unix::fs::PermissionsExt;
790
791 let temporary = tempfile::tempdir()?;
792 let script = temporary.path().join("codegraph-test");
793 let descendant_pid = temporary.path().join("descendant.pid");
794 std::fs::write(
795 &script,
796 format!(
797 "#!/bin/sh\nsleep 30 &\nprintf '%s' \"$!\" > '{}'\nwait\n",
798 descendant_pid.display()
799 ),
800 )?;
801 std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o700))?;
802
803 let error = run_codegraph_sync(
804 script.as_os_str(),
805 temporary.path(),
806 Instant::now() + Duration::from_secs(2),
807 )
808 .expect_err("test process must time out");
809 assert!(
810 error.contains("was terminated"),
811 "unexpected error: {error}"
812 );
813 let pid = std::fs::read_to_string(&descendant_pid)?.parse::<i32>()?;
814 for _ in 0..20 {
815 if nix::sys::signal::kill(nix::unistd::Pid::from_raw(pid), None).is_err() {
816 return Ok(());
817 }
818 std::thread::sleep(Duration::from_millis(10));
819 }
820 anyhow::bail!("CodeGraph descendant {pid} survived timeout")
821 }
822
823 #[cfg(unix)]
824 #[test]
825 fn successful_codegraph_sync_should_terminate_surviving_descendants() -> anyhow::Result<()> {
826 use std::os::unix::fs::PermissionsExt;
827
828 let temporary = tempfile::tempdir()?;
829 let script = temporary.path().join("codegraph-test");
830 let descendant_pid = temporary.path().join("descendant.pid");
831 std::fs::write(
832 &script,
833 format!(
834 "#!/bin/sh\nsh -c 'trap \"\" TERM; sleep 30' &\nprintf '%s' \"$!\" > '{}'\nexit 0\n",
835 descendant_pid.display()
836 ),
837 )?;
838 std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o700))?;
839
840 run_codegraph_sync(
841 script.as_os_str(),
842 temporary.path(),
843 Instant::now() + Duration::from_secs(2),
844 )
845 .map_err(anyhow::Error::msg)?;
846 let pid = std::fs::read_to_string(&descendant_pid)?.parse::<i32>()?;
847 for _ in 0..20 {
848 if nix::sys::signal::kill(nix::unistd::Pid::from_raw(pid), None).is_err() {
849 return Ok(());
850 }
851 std::thread::sleep(Duration::from_millis(10));
852 }
853 anyhow::bail!("CodeGraph descendant {pid} survived successful sync")
854 }
855}