Skip to content

Commit 622af92

Browse files
committed
fix: handle Trace2 conflicts and concurrent connections
1 parent 677a8d4 commit 622af92

10 files changed

Lines changed: 429 additions & 47 deletions

File tree

‎src-tauri/src/core.rs‎

Lines changed: 158 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -358,10 +358,12 @@ impl AppCore {
358358
Ok(conflicts) => (conflicts.len(), None),
359359
Err(error) => (0, Some(error)),
360360
};
361+
let expected_trace2_target =
362+
format!("af_unix:stream:{}", self.paths.trace_socket.display());
363+
let trace2_target_overridden = settings.acceleration_enabled
364+
&& git::effective_trace2_target().ok().flatten().as_deref()
365+
!= Some(expected_trace2_target.as_str());
361366
Ok(AppSnapshot {
362-
settings,
363-
nodes,
364-
routes,
365367
environment: EnvironmentSummary {
366368
git_available: git::git_version().is_some(),
367369
git_path: git::git_path(),
@@ -370,7 +372,11 @@ impl AppCore {
370372
config_path: self.paths.gitconfig.display().to_string(),
371373
conflicts,
372374
conflict_scan_error,
375+
trace2_target_overridden,
373376
},
377+
settings,
378+
nodes,
379+
routes,
374380
})
375381
}
376382

@@ -907,7 +913,15 @@ impl AppCore {
907913
self.snapshot()
908914
}
909915

910-
pub fn set_acceleration(&self, enabled: bool) -> Result<AppSnapshot, String> {
916+
pub fn trace2_target_conflict(&self) -> Result<Option<String>, String> {
917+
git::external_trace2_target(&self.paths.gitconfig)
918+
}
919+
920+
pub fn set_acceleration(
921+
&self,
922+
enabled: bool,
923+
replace_trace2_target: bool,
924+
) -> Result<AppSnapshot, String> {
911925
let _guard = self.lock.lock();
912926
let mut settings = self.settings()?;
913927
if enabled && git::git_version().is_none() {
@@ -919,6 +933,12 @@ impl AppCore {
919933
if enabled && settings.route_scope == RouteScope::Allowlist && self.routes()?.is_empty() {
920934
return Err("仅加速清单为空,请先加入至少一个公开仓库".into());
921935
}
936+
if enabled
937+
&& !replace_trace2_target
938+
&& git::external_trace2_target(&self.paths.gitconfig)?.is_some()
939+
{
940+
return Err("检测到其他工具正在使用 Git Trace2,请在 GitBoost 窗口中选择保留现有配置或切换到 GitBoost".into());
941+
}
922942
if enabled {
923943
settings.acceleration_enabled = true;
924944
} else {
@@ -1040,6 +1060,8 @@ impl AppCore {
10401060
git::build_config(settings, selected, routes, Some(&self.paths.trace_socket))?;
10411061
let previous = fs::read(&self.paths.gitconfig).ok();
10421062
let registered_before = git::include_registered(&self.paths.gitconfig);
1063+
let preserve_external_trace = !settings.acceleration_enabled
1064+
&& git::external_trace2_target(&self.paths.gitconfig)?.is_some();
10431065
if settings.acceleration_enabled {
10441066
let mut candidate = NamedTempFile::new_in(&self.paths.root)
10451067
.map_err(|error| format!("无法创建配置候选文件:{error}"))?;
@@ -1063,7 +1085,17 @@ impl AppCore {
10631085
"gitboost.gitconfig",
10641086
);
10651087
atomic_write(&self.paths.gitconfig, content.as_bytes())?;
1066-
if settings.acceleration_enabled || registered_before {
1088+
if preserve_external_trace {
1089+
if let Err(error) = git::unregister_include(&self.paths.gitconfig) {
1090+
self.restore_git_state(previous.as_deref(), registered_before);
1091+
return Err(error);
1092+
}
1093+
} else if settings.acceleration_enabled {
1094+
if let Err(error) = git::register_include_last(&self.paths.gitconfig) {
1095+
self.restore_git_state(previous.as_deref(), registered_before);
1096+
return Err(error);
1097+
}
1098+
} else if registered_before {
10671099
if let Err(error) = git::register_include(&self.paths.gitconfig) {
10681100
self.restore_git_state(previous.as_deref(), registered_before);
10691101
return Err(error);
@@ -2805,7 +2837,7 @@ mod tests {
28052837
core.acknowledge_consent().unwrap();
28062838

28072839
assert_eq!(
2808-
core.set_acceleration(true).unwrap_err(),
2840+
core.set_acceleration(true, false).unwrap_err(),
28092841
"没有通过真实 Git 检测的可用节点"
28102842
);
28112843
let unchanged = core.settings().unwrap();
@@ -2827,30 +2859,142 @@ mod tests {
28272859
)
28282860
.unwrap();
28292861

2830-
let automatic = core.set_acceleration(true).unwrap();
2862+
let automatic = core.set_acceleration(true, false).unwrap();
28312863
assert!(automatic.settings.acceleration_enabled);
28322864
assert_eq!(
28332865
automatic.settings.current_node_id.as_deref(),
28342866
Some(FASTGIT_REWRITE_BASE)
28352867
);
28362868

2837-
let direct = core.set_acceleration(false).unwrap();
2869+
let direct = core.set_acceleration(false, false).unwrap();
28382870
assert!(!direct.settings.acceleration_enabled);
28392871
assert!(direct.settings.current_node_id.is_none());
28402872

2841-
core.set_acceleration(true).unwrap();
2873+
core.set_acceleration(true, false).unwrap();
28422874
let route_id = core.routes().unwrap()[0].id.clone();
28432875
let empty_allowlist = core.delete_route(&route_id).unwrap();
28442876
assert!(!empty_allowlist.settings.acceleration_enabled);
28452877
assert!(empty_allowlist.settings.current_node_id.is_none());
28462878

28472879
core.set_route_scope(RouteScope::Global).unwrap();
2848-
core.set_acceleration(true).unwrap();
2880+
core.set_acceleration(true, false).unwrap();
28492881
let restored_allowlist = core.set_route_scope(RouteScope::Allowlist).unwrap();
28502882
assert!(!restored_allowlist.settings.acceleration_enabled);
28512883
assert!(restored_allowlist.settings.current_node_id.is_none());
28522884
}
28532885

2886+
#[test]
2887+
fn trace2_conflict_requires_a_choice_and_restores_the_external_target() {
2888+
const MARKER: &str = "GITBOOST_TRACE2_CONFLICT_CHOICE_CHILD";
2889+
if rerun_with_isolated_git(
2890+
"core::tests::trace2_conflict_requires_a_choice_and_restores_the_external_target",
2891+
MARKER,
2892+
) {
2893+
return;
2894+
}
2895+
let external_target = "af_unix:stream:/tmp/external-trace2.sock";
2896+
let configured = std::process::Command::new("git")
2897+
.args(["config", "--global", "trace2.eventTarget", external_target])
2898+
.status()
2899+
.unwrap();
2900+
assert!(configured.success());
2901+
2902+
let directory = tempfile::tempdir().unwrap();
2903+
let core = AppCore::new(directory.path().to_path_buf()).unwrap();
2904+
core.add_route("openai/codex").unwrap();
2905+
core.acknowledge_consent().unwrap();
2906+
atomic_write_json(
2907+
&core.paths.health,
2908+
&HashMap::from([(
2909+
FASTGIT_REWRITE_BASE.to_string(),
2910+
HealthSummary {
2911+
status: NodeStatus::Available,
2912+
in_auto_pool: true,
2913+
success_count: 1,
2914+
attempt_count: 1,
2915+
..HealthSummary::default()
2916+
},
2917+
)]),
2918+
)
2919+
.unwrap();
2920+
2921+
assert_eq!(
2922+
core.trace2_target_conflict().unwrap().as_deref(),
2923+
Some(external_target)
2924+
);
2925+
assert!(core
2926+
.set_acceleration(true, false)
2927+
.unwrap_err()
2928+
.contains("Trace2"));
2929+
assert!(!core.settings().unwrap().acceleration_enabled);
2930+
2931+
let enabled = core.set_acceleration(true, true).unwrap();
2932+
assert!(enabled.settings.acceleration_enabled);
2933+
assert!(enabled.environment.include_registered);
2934+
assert!(!enabled.environment.trace2_target_overridden);
2935+
let effective = std::process::Command::new("git")
2936+
.args([
2937+
"config",
2938+
"--global",
2939+
"--includes",
2940+
"--get",
2941+
"trace2.eventTarget",
2942+
])
2943+
.output()
2944+
.unwrap();
2945+
assert!(effective.status.success());
2946+
assert_eq!(
2947+
String::from_utf8_lossy(&effective.stdout).trim(),
2948+
format!("af_unix:stream:{}", core.paths.trace_socket.display())
2949+
);
2950+
2951+
let external_config = directory.path().join("external.gitconfig");
2952+
fs::write(
2953+
&external_config,
2954+
format!("[trace2]\n\teventTarget = {external_target}\n"),
2955+
)
2956+
.unwrap();
2957+
let overridden = std::process::Command::new("git")
2958+
.args([
2959+
"config",
2960+
"--global",
2961+
"--add",
2962+
"include.path",
2963+
external_config.to_string_lossy().as_ref(),
2964+
])
2965+
.status()
2966+
.unwrap();
2967+
assert!(overridden.success());
2968+
assert!(
2969+
core.snapshot()
2970+
.unwrap()
2971+
.environment
2972+
.trace2_target_overridden
2973+
);
2974+
2975+
let reclaimed = core.set_acceleration(true, true).unwrap();
2976+
assert!(!reclaimed.environment.trace2_target_overridden);
2977+
2978+
let disabled = core.set_acceleration(false, false).unwrap();
2979+
assert!(!disabled.settings.acceleration_enabled);
2980+
assert!(!disabled.environment.include_registered);
2981+
let restored = std::process::Command::new("git")
2982+
.args([
2983+
"config",
2984+
"--global",
2985+
"--includes",
2986+
"--get",
2987+
"trace2.eventTarget",
2988+
])
2989+
.output()
2990+
.unwrap();
2991+
assert!(restored.status.success());
2992+
assert_eq!(
2993+
String::from_utf8_lossy(&restored.stdout).trim(),
2994+
external_target
2995+
);
2996+
}
2997+
28542998
#[test]
28552999
fn acceleration_requires_privacy_consent_once() {
28563000
const MARKER: &str = "GITBOOST_CONSENT_GATE_STATE_CHILD";
@@ -2867,7 +3011,7 @@ mod tests {
28673011
assert!(core.settings().unwrap().consent_acknowledged_at.is_none());
28683012

28693013
assert_eq!(
2870-
core.set_acceleration(true).unwrap_err(),
3014+
core.set_acceleration(true, false).unwrap_err(),
28713015
"首次开启加速前,请在 GitBoost 窗口中确认隐私提示"
28723016
);
28733017
assert!(!core.settings().unwrap().acceleration_enabled);
@@ -2878,7 +3022,7 @@ mod tests {
28783022

28793023
// 同意后不再要求确认,错误回到线路本身
28803024
assert_eq!(
2881-
core.set_acceleration(true).unwrap_err(),
3025+
core.set_acceleration(true, false).unwrap_err(),
28823026
"没有通过真实 Git 检测的可用节点"
28833027
);
28843028
}
@@ -3505,7 +3649,7 @@ mod tests {
35053649

35063650
core.add_route("openai/codex").unwrap();
35073651
assert_eq!(
3508-
core.set_acceleration(true).unwrap_err(),
3652+
core.set_acceleration(true, false).unwrap_err(),
35093653
"首次开启加速前,请在 GitBoost 窗口中确认隐私提示"
35103654
);
35113655
core.acknowledge_consent().unwrap();
@@ -3514,7 +3658,7 @@ mod tests {
35143658
// 真实文件持久化和重新加载后,同意状态仍然有效,不依赖进程内 mock 状态。
35153659
let core = AppCore::new(root.clone()).unwrap();
35163660
assert!(core.settings().unwrap().consent_acknowledged_at.is_some());
3517-
let enabled = core.set_acceleration(true).unwrap();
3661+
let enabled = core.set_acceleration(true, false).unwrap();
35183662
assert!(enabled.settings.acceleration_enabled);
35193663
let node_id = enabled
35203664
.settings

‎src-tauri/src/git.rs‎

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,11 @@ pub fn register_include(config_path: &Path) -> Result<(), String> {
311311
}
312312
}
313313

314+
pub fn register_include_last(config_path: &Path) -> Result<(), String> {
315+
unregister_include(config_path)?;
316+
register_include(config_path)
317+
}
318+
314319
pub fn unregister_include(config_path: &Path) -> Result<(), String> {
315320
let mut values = matching_include_values(config_path)?;
316321
values.sort();
@@ -339,6 +344,79 @@ pub fn unregister_include(config_path: &Path) -> Result<(), String> {
339344
}
340345
}
341346

347+
pub fn external_trace2_target(config_path: &Path) -> Result<Option<String>, String> {
348+
let targets = configured_trace2_targets()?;
349+
let mut external = None;
350+
for (origin, target) in targets {
351+
let managed = origin
352+
.strip_prefix("file:")
353+
.map(Path::new)
354+
.filter(|path| path.is_absolute())
355+
.is_some_and(|path| same_config_file(path, config_path).unwrap_or(false));
356+
if !managed {
357+
external = Some(target);
358+
}
359+
}
360+
Ok(external.filter(|target| trace2_target_enabled(target)))
361+
}
362+
363+
pub fn effective_trace2_target() -> Result<Option<String>, String> {
364+
Ok(configured_trace2_targets()?
365+
.pop()
366+
.map(|(_, target)| target)
367+
.filter(|target| trace2_target_enabled(target)))
368+
}
369+
370+
fn configured_trace2_targets() -> Result<Vec<(String, String)>, String> {
371+
let mut targets = trace2_targets("--system")?;
372+
targets.extend(trace2_targets("--global")?);
373+
Ok(targets)
374+
}
375+
376+
fn trace2_target_enabled(target: &str) -> bool {
377+
let target = target.trim();
378+
!target.is_empty() && target != "0" && !target.eq_ignore_ascii_case("false")
379+
}
380+
381+
fn trace2_targets(scope: &str) -> Result<Vec<(String, String)>, String> {
382+
let output = run_git(
383+
[
384+
"config",
385+
scope,
386+
"--includes",
387+
"--show-origin",
388+
"--null",
389+
"--get-all",
390+
"trace2.eventTarget",
391+
],
392+
None,
393+
Duration::from_secs(5),
394+
)?;
395+
if output.status.code() == Some(1) {
396+
return Ok(vec![]);
397+
}
398+
if !output.status.success() {
399+
return Err(command_error(&output));
400+
}
401+
let fields: Vec<_> = output
402+
.stdout
403+
.split(|byte| *byte == 0)
404+
.filter(|field| !field.is_empty())
405+
.collect();
406+
if !fields.len().is_multiple_of(2) {
407+
return Err("无法解析 Git Trace2 配置来源".into());
408+
}
409+
Ok(fields
410+
.chunks_exact(2)
411+
.map(|pair| {
412+
(
413+
String::from_utf8_lossy(pair[0]).into_owned(),
414+
String::from_utf8_lossy(pair[1]).into_owned(),
415+
)
416+
})
417+
.collect())
418+
}
419+
342420
pub fn find_conflicts(
343421
config_path: &Path,
344422
repository_path: Option<&Path>,

0 commit comments

Comments
 (0)