Files
zed/crates/settings/src/settings_file.rs
T
Ben Kunkle a9e84fbb8c Fix config file watch task leak (#47246)
Follow-Up-For:  #47243

Previously, we would detach tasks spawned to watch config files.
However, the task blocked on receiving a file event before checking if
the receiver for the updates channel was dropped, causing the task to
never exit. The fix here was to return the task explicitly, so that it
can be dropped instead of calling `.detach()` on it. There is definitely
a way to `select!` between the receiver being dropped and the next file
system event, but I couldn't figure it out in a reasonable amount of
time and decided it wasn't worth it.

Release Notes:

- Fixed an issue where a few file descriptors would be leaked each time
a project was closed
2026-01-20 15:58:35 -05:00

160 lines
5.2 KiB
Rust

use crate::{settings_content::SettingsContent, settings_store::SettingsStore};
use collections::HashSet;
use fs::{Fs, PathEventKind};
use futures::{StreamExt, channel::mpsc};
use gpui::{App, BackgroundExecutor, ReadGlobal};
use std::{path::PathBuf, sync::Arc, time::Duration};
pub const EMPTY_THEME_NAME: &str = "empty-theme";
/// Settings for visual tests that use proper fonts instead of Courier.
/// Uses Helvetica Neue for UI (sans-serif) and Menlo for code (monospace),
/// which are available on all macOS systems.
#[cfg(any(test, feature = "test-support"))]
pub fn visual_test_settings() -> String {
let mut value =
crate::parse_json_with_comments::<serde_json::Value>(crate::default_settings().as_ref())
.unwrap();
util::merge_non_null_json_value_into(
serde_json::json!({
"ui_font_family": ".SystemUIFont",
"ui_font_features": {},
"ui_font_size": 14,
"ui_font_fallback": [],
"buffer_font_family": "Menlo",
"buffer_font_features": {},
"buffer_font_size": 14,
"buffer_font_fallbacks": [],
"theme": EMPTY_THEME_NAME,
}),
&mut value,
);
value.as_object_mut().unwrap().remove("languages");
serde_json::to_string(&value).unwrap()
}
#[cfg(any(test, feature = "test-support"))]
pub fn test_settings() -> String {
let mut value =
crate::parse_json_with_comments::<serde_json::Value>(crate::default_settings().as_ref())
.unwrap();
#[cfg(not(target_os = "windows"))]
util::merge_non_null_json_value_into(
serde_json::json!({
"ui_font_family": "Courier",
"ui_font_features": {},
"ui_font_size": 14,
"ui_font_fallback": [],
"buffer_font_family": "Courier",
"buffer_font_features": {},
"buffer_font_size": 14,
"buffer_font_fallbacks": [],
"theme": EMPTY_THEME_NAME,
}),
&mut value,
);
#[cfg(target_os = "windows")]
util::merge_non_null_json_value_into(
serde_json::json!({
"ui_font_family": "Courier New",
"ui_font_features": {},
"ui_font_size": 14,
"ui_font_fallback": [],
"buffer_font_family": "Courier New",
"buffer_font_features": {},
"buffer_font_size": 14,
"buffer_font_fallbacks": [],
"theme": EMPTY_THEME_NAME,
}),
&mut value,
);
value.as_object_mut().unwrap().remove("languages");
serde_json::to_string(&value).unwrap()
}
pub fn watch_config_file(
executor: &BackgroundExecutor,
fs: Arc<dyn Fs>,
path: PathBuf,
) -> (mpsc::UnboundedReceiver<String>, gpui::Task<()>) {
let (tx, rx) = mpsc::unbounded();
let task = executor.spawn(async move {
let (events, _) = fs.watch(&path, Duration::from_millis(100)).await;
futures::pin_mut!(events);
let contents = fs.load(&path).await.unwrap_or_default();
if tx.unbounded_send(contents).is_err() {
return;
}
loop {
if events.next().await.is_none() {
break;
}
if let Ok(contents) = fs.load(&path).await
&& tx.unbounded_send(contents).is_err()
{
break;
}
}
});
(rx, task)
}
pub fn watch_config_dir(
executor: &BackgroundExecutor,
fs: Arc<dyn Fs>,
dir_path: PathBuf,
config_paths: HashSet<PathBuf>,
) -> mpsc::UnboundedReceiver<String> {
let (tx, rx) = mpsc::unbounded();
executor
.spawn(async move {
for file_path in &config_paths {
if fs.metadata(file_path).await.is_ok_and(|v| v.is_some())
&& let Ok(contents) = fs.load(file_path).await
&& tx.unbounded_send(contents).is_err()
{
return;
}
}
let (events, _) = fs.watch(&dir_path, Duration::from_millis(100)).await;
futures::pin_mut!(events);
while let Some(event_batch) = events.next().await {
for event in event_batch {
if config_paths.contains(&event.path) {
match event.kind {
Some(PathEventKind::Removed) => {
if tx.unbounded_send(String::new()).is_err() {
return;
}
}
Some(PathEventKind::Created) | Some(PathEventKind::Changed) => {
if let Ok(contents) = fs.load(&event.path).await
&& tx.unbounded_send(contents).is_err()
{
return;
}
}
_ => {}
}
}
}
}
})
.detach();
rx
}
pub fn update_settings_file(
fs: Arc<dyn Fs>,
cx: &App,
update: impl 'static + Send + FnOnce(&mut SettingsContent, &App),
) {
SettingsStore::global(cx).update_settings_file(fs, update);
}