Skip to content

Commit 32a2ab8

Browse files
committed
fix: seed retained empty file provider domains
1 parent 64a669b commit 32a2ab8

3 files changed

Lines changed: 107 additions & 32 deletions

File tree

apps/desktop/src-tauri/src/main.rs

Lines changed: 86 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8801,14 +8801,26 @@ fn activate_virtual_projection_mount(
88018801
wait_for_virtual_projection_mount_point_children(state_root, mount)?;
88028802
}
88038803
let macos_domain_registration = register_virtual_projection(state_root, mount)?;
8804+
let seed_macos_working_set =
8805+
virtual_projection_mount_activation_seeds_working_set(mount, macos_domain_registration);
8806+
if seed_macos_working_set {
8807+
desktop_log(
8808+
"info",
8809+
"file_provider.working_set_seed",
8810+
format!(
8811+
"seeding macOS File Provider working set for mount `{}` because the shared domain is newly registered or has no visible source folders",
8812+
mount.mount_id.0
8813+
),
8814+
);
8815+
}
88048816
prefetch_virtual_projection_root(state_root, mount)?;
88058817
if wait_for_entities
88068818
&& !virtual_projection_waits_for_mount_point_children_before_registration(&mount.projection)
88078819
{
88088820
wait_for_mount_entities(state_root, &mount.mount_id)?;
88098821
}
88108822
ensure_virtual_projection_runtime(state_root, mount)?;
8811-
refresh_virtual_projection_mount_activation(mount, macos_domain_registration);
8823+
refresh_virtual_projection_mount_activation(mount, seed_macos_working_set);
88128824
recover_virtual_projection_mount_root_if_needed(state_root, mount)?;
88138825
desktop_log(
88148826
"info",
@@ -8885,7 +8897,11 @@ fn recover_macos_file_provider_mount_root_if_needed(
88858897
);
88868898
prefetch_virtual_projection_root(state_root, mount)?;
88878899
ensure_virtual_projection_runtime(state_root, mount)?;
8888-
refresh_virtual_projection_mount_activation(mount, MacosFileProviderDomainRegistration::Reused);
8900+
let seed_macos_working_set = virtual_projection_mount_activation_seeds_working_set(
8901+
mount,
8902+
MacosFileProviderDomainRegistration::Reused,
8903+
);
8904+
refresh_virtual_projection_mount_activation(mount, seed_macos_working_set);
88898905

88908906
wait_for_macos_file_provider_mount_root_recovery(
88918907
&root,
@@ -9806,11 +9822,9 @@ fn signal_virtual_projection_refresh(mount: &MountConfig) {
98069822
}
98079823
}
98089824

9809-
fn refresh_virtual_projection_mount_activation(
9810-
mount: &MountConfig,
9811-
domain_registration: MacosFileProviderDomainRegistration,
9812-
) {
9813-
for action in virtual_projection_mount_activation_refresh_actions(mount, domain_registration) {
9825+
fn refresh_virtual_projection_mount_activation(mount: &MountConfig, seed_macos_working_set: bool) {
9826+
for action in virtual_projection_mount_activation_refresh_actions(mount, seed_macos_working_set)
9827+
{
98149828
run_virtual_projection_refresh_action(mount, &action);
98159829
}
98169830
}
@@ -9848,14 +9862,14 @@ fn virtual_projection_refresh_actions(mount: &MountConfig) -> Vec<VirtualProject
98489862

98499863
fn virtual_projection_mount_activation_refresh_actions(
98509864
mount: &MountConfig,
9851-
domain_registration: MacosFileProviderDomainRegistration,
9865+
seed_macos_working_set: bool,
98529866
) -> Vec<VirtualProjectionRefreshAction> {
98539867
if mount.projection == ProjectionMode::MacosFileProvider {
98549868
let mut actions = vec![
98559869
VirtualProjectionRefreshAction::Signal(ROOT_CONTAINER_IDENTIFIER.to_string()),
98569870
VirtualProjectionRefreshAction::ReimportThenSignal(mount_point_identifier(mount)),
98579871
];
9858-
if domain_registration == MacosFileProviderDomainRegistration::Created {
9872+
if seed_macos_working_set {
98599873
actions.push(VirtualProjectionRefreshAction::Signal(
98609874
"working-set".to_string(),
98619875
));
@@ -9865,6 +9879,38 @@ fn virtual_projection_mount_activation_refresh_actions(
98659879
virtual_projection_refresh_actions(mount)
98669880
}
98679881

9882+
fn virtual_projection_mount_activation_seeds_working_set(
9883+
mount: &MountConfig,
9884+
domain_registration: MacosFileProviderDomainRegistration,
9885+
) -> bool {
9886+
if mount.projection != ProjectionMode::MacosFileProvider {
9887+
return false;
9888+
}
9889+
if domain_registration == MacosFileProviderDomainRegistration::Created {
9890+
return true;
9891+
}
9892+
9893+
mount_access_root(mount)
9894+
.parent()
9895+
.is_some_and(macos_file_provider_domain_root_is_visibly_empty)
9896+
}
9897+
9898+
fn macos_file_provider_domain_root_is_visibly_empty(root: &Path) -> bool {
9899+
let entries = match fs::read_dir(root) {
9900+
Ok(entries) => entries,
9901+
Err(error) => return error.kind() == io::ErrorKind::NotFound,
9902+
};
9903+
for entry in entries {
9904+
let Ok(entry) = entry else {
9905+
return false;
9906+
};
9907+
if !entry.file_name().to_string_lossy().starts_with('.') {
9908+
return false;
9909+
}
9910+
}
9911+
true
9912+
}
9913+
98689914
fn refresh_virtual_projection_container(
98699915
mount: &MountConfig,
98709916
action: &VirtualProjectionRefreshAction,
@@ -12087,7 +12133,8 @@ mod tests {
1208712133
live_mode_should_reconcile_local_target_for_key, live_mode_target,
1208812134
live_mode_tick_from_snapshot, live_mode_wake_generation, load_desktop_activity,
1208912135
macos_app_bundle_for_exe, macos_file_provider_child_item_count,
12090-
macos_file_provider_domain_registration, macos_file_provider_mount_root_health_error,
12136+
macos_file_provider_domain_registration, macos_file_provider_domain_root_is_visibly_empty,
12137+
macos_file_provider_mount_root_health_error,
1209112138
macos_file_provider_mount_root_inspection_recovery_reason,
1209212139
macos_file_provider_mount_root_is_missing, macos_file_provider_mount_root_recovery_reason,
1209312140
mark_mount_live_mode_syncing, mount_has_pending_local_changes,
@@ -15050,10 +15097,7 @@ mod tests {
1505015097
.projection(ProjectionMode::MacosFileProvider);
1505115098

1505215099
assert_eq!(
15053-
virtual_projection_mount_activation_refresh_actions(
15054-
&mount,
15055-
MacosFileProviderDomainRegistration::Reused,
15056-
),
15100+
virtual_projection_mount_activation_refresh_actions(&mount, false),
1505715101
vec![
1505815102
VirtualProjectionRefreshAction::Signal("root".to_string()),
1505915103
VirtualProjectionRefreshAction::ReimportThenSignal(
@@ -15064,7 +15108,7 @@ mod tests {
1506415108
}
1506515109

1506615110
#[test]
15067-
fn macos_file_provider_new_domain_seeds_the_working_set_once() {
15111+
fn macos_file_provider_new_domain_seeds_the_working_set() {
1506815112
let mount = MountConfig::new(
1506915113
MountId::new("notion-main"),
1507015114
"notion",
@@ -15073,10 +15117,7 @@ mod tests {
1507315117
.projection(ProjectionMode::MacosFileProvider);
1507415118

1507515119
assert_eq!(
15076-
virtual_projection_mount_activation_refresh_actions(
15077-
&mount,
15078-
MacosFileProviderDomainRegistration::Created,
15079-
),
15120+
virtual_projection_mount_activation_refresh_actions(&mount, true),
1508015121
vec![
1508115122
VirtualProjectionRefreshAction::Signal("root".to_string()),
1508215123
VirtualProjectionRefreshAction::ReimportThenSignal("mount:notion-main".to_string(),),
@@ -15085,6 +15126,32 @@ mod tests {
1508515126
);
1508615127
}
1508715128

15129+
#[test]
15130+
fn macos_file_provider_retained_empty_domain_requires_working_set_seed() {
15131+
let temp = TestTempDir::new("desktop-file-provider-retained-empty-domain");
15132+
let domain_root = temp.path().join("Locality");
15133+
fs::create_dir_all(domain_root.join(".Trash")).expect("create File Provider trash");
15134+
fs::write(domain_root.join(".DS_Store"), "metadata").expect("create hidden metadata");
15135+
15136+
assert!(macos_file_provider_domain_root_is_visibly_empty(
15137+
&domain_root
15138+
));
15139+
15140+
fs::create_dir(domain_root.join("notion")).expect("create visible Notion mount");
15141+
assert!(!macos_file_provider_domain_root_is_visibly_empty(
15142+
&domain_root
15143+
));
15144+
}
15145+
15146+
#[test]
15147+
fn macos_file_provider_missing_domain_root_requires_working_set_seed() {
15148+
let temp = TestTempDir::new("desktop-file-provider-missing-domain-root");
15149+
15150+
assert!(macos_file_provider_domain_root_is_visibly_empty(
15151+
&temp.path().join("Locality")
15152+
));
15153+
}
15154+
1508815155
#[test]
1508915156
fn macos_file_provider_registration_report_defaults_to_reused() {
1509015157
assert_eq!(

docs/desktop-app.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -213,12 +213,17 @@ Later source mounts reuse the existing shared File Provider domain. Adding a
213213
top-level source folder such as `google-calendar-main` signals the domain root
214214
enumerator so macOS can discover that one child, but must not reimport the
215215
shared root, replace its registration, or disturb sibling mounts such as
216-
`notion`. A newly created domain also receives one non-destructive working-set
216+
`notion`. A newly created domain also receives a non-destructive working-set
217217
signal after its first source is ready; macOS requires that initial enumeration
218-
to seed the otherwise-empty File Provider database. Later source activation
219-
does not repeat the working-set signal. Registration is idempotent when the
220-
shared domain already exists; automatic setup treats that registration as
221-
authoritative rather than removing and recreating it to repair metadata.
218+
to seed the otherwise-empty File Provider database. macOS can retain a domain
219+
registration while discarding all of its visible source folders, so activation
220+
also seeds a reused domain when its root contains no visible entries other than
221+
system-owned hidden items such as `.Trash`. The scoped retry repeats that seed
222+
only while the root remains empty. Later source activation does not signal the
223+
working set when any sibling source folder is visible. Registration is
224+
idempotent when the shared domain already exists; automatic setup treats that
225+
registration as authoritative rather than removing and recreating it to repair
226+
metadata.
222227
Reimport and readiness repair stay scoped to the new mount-point identifier.
223228
Mount-point appearance gets an initial 30-second wait and one 30-second scoped
224229
refresh window; if it is still unavailable, Locality reports a recoverable

platform/macos/LocalityFileProvider/README.md

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -70,14 +70,17 @@ Finder must enter the File Provider domain so directory listings call
7070
`file_provider_children` on `localityd`.
7171

7272
Mount activation signals the shared domain root after adding a source. A newly
73-
registered domain also receives one working-set signal to seed macOS's initially
74-
empty File Provider database. Later source mounts do not repeat that global
75-
signal: their reimport and readiness repair stay scoped to the new mount-point
76-
identifier. Because macOS creates a source folder asynchronously, Locality waits
77-
for it before inspecting it and retries the scoped refresh once. Automatic
78-
activation never resets or re-registers the shared domain. Reconnecting an
79-
existing source retries this activation path instead of only reloading daemon
80-
mounts.
73+
registered domain also receives a working-set signal to seed macOS's initially
74+
empty File Provider database. Because macOS may retain the registration while
75+
discarding its visible source folders, Locality applies the same seed to a reused
76+
domain whose root has only hidden system items such as `.Trash`. The scoped
77+
retry repeats the seed only while that root remains empty. When a sibling source
78+
folder is visible, later source mounts do not send the global signal: their
79+
reimport and readiness repair stay scoped to the new mount-point identifier.
80+
Because macOS creates a source folder asynchronously, Locality waits for it
81+
before inspecting it and retries the scoped refresh once. Automatic activation
82+
never resets or re-registers the shared domain. Reconnecting an existing source
83+
retries this activation path instead of only reloading daemon mounts.
8184

8285
Delete support still returns unsupported. Creates and renames are represented as
8386
daemon virtual mutations and stay pending until the normal review and push flow

0 commit comments

Comments
 (0)