Skip to content

Commit b906267

Browse files
authored
Merge pull request #10356 from nextcloud/i2h3/findersync-load-improvements
Improve FinderSync Extension handling
2 parents a0d37d9 + 0e32cd7 commit b906267

7 files changed

Lines changed: 364 additions & 38 deletions

File tree

admin/osx/post_install.sh.cmake

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,57 @@
44
# SPDX-FileCopyrightText: 2015 ownCloud GmbH
55
# SPDX-License-Identifier: GPL-2.0-or-later
66

7-
# Always enable the new 10.10 finder plugin if available
8-
if [ -x "$(command -v pluginkit)" ]; then
9-
# add it to DB. This happens automatically too but we try to push it a bit harder for issue #3463
10-
pluginkit -a "/Applications/@APPLICATION_NAME@.app/Contents/PlugIns/FinderSyncExt.appex/"
11-
# Since El Capitan we need to sleep #4650
12-
sleep 10s
13-
# enable it
14-
pluginkit -e use -i @APPLICATION_REV_DOMAIN@.FinderSyncExt
7+
# Register and enable the Finder Sync extension.
8+
#
9+
# pluginkit manages the PlugInKit subsystem *per user*, but this postinstall script
10+
# runs as root. Running pluginkit directly here mutates root's registry and is a no-op
11+
# for the logged-in user — the extension then never appears/enables for them (issues
12+
# #8471, #10032). So we hop into the console user's context (launchctl asuser + sudo -u)
13+
# before calling pluginkit. Every step logs its result to /var/log/install.log so a
14+
# failed registration is recognisable from installer logs users provide.
15+
APPEX="/Applications/@APPLICATION_NAME@.app/Contents/PlugIns/FinderSyncExt.appex/"
16+
EXT_ID="@APPLICATION_REV_DOMAIN@.FinderSyncExt"
17+
LOG_PREFIX="Nextcloud FinderSync:"
18+
19+
CONSOLE_USER=$(stat -f%Su /dev/console 2>/dev/null)
20+
CONSOLE_UID=$(id -u "$CONSOLE_USER" 2>/dev/null)
21+
22+
run_as_console_user() {
23+
launchctl asuser "$CONSOLE_UID" sudo -u "$CONSOLE_USER" "$@"
24+
}
25+
26+
if [ -z "$CONSOLE_USER" ] || [ "$CONSOLE_USER" = "root" ] || [ -z "$CONSOLE_UID" ] || [ "$CONSOLE_UID" -eq 0 ]; then
27+
echo "$LOG_PREFIX no console user logged in; skipping pluginkit registration (extension will register on first launch of the app by the user)"
28+
elif [ ! -x "$(command -v pluginkit)" ]; then
29+
echo "$LOG_PREFIX pluginkit not found; cannot register extension for user '$CONSOLE_USER'"
30+
else
31+
echo "$LOG_PREFIX registering extension for user '$CONSOLE_USER' (uid $CONSOLE_UID)"
32+
33+
# Add it to the DB. This happens automatically too, but we push a bit harder (#3463).
34+
run_as_console_user pluginkit -a "$APPEX"; rc=$?
35+
if [ "$rc" -eq 0 ]; then
36+
echo "$LOG_PREFIX pluginkit -a succeeded for $APPEX"
37+
else
38+
echo "$LOG_PREFIX WARNING pluginkit -a failed (rc $rc) for $APPEX"
39+
fi
40+
41+
# Since El Capitan we need to wait for discovery before electing (#4650).
42+
sleep 10
43+
44+
# Enable (elect) it for the console user.
45+
run_as_console_user pluginkit -e use -i "$EXT_ID"; rc=$?
46+
if [ "$rc" -eq 0 ]; then
47+
echo "$LOG_PREFIX pluginkit -e use succeeded for $EXT_ID"
48+
else
49+
echo "$LOG_PREFIX WARNING pluginkit -e use failed (rc $rc) for $EXT_ID"
50+
fi
51+
52+
# Record the resulting election state for support diagnostics.
53+
if run_as_console_user pluginkit -m -i "$EXT_ID" -v; then
54+
echo "$LOG_PREFIX extension present in pluginkit database after registration (see line above for election state)"
55+
else
56+
echo "$LOG_PREFIX WARNING extension NOT present in pluginkit database after registration for $EXT_ID"
57+
fi
1558
fi
1659

1760
# Remove legacy LaunchAgent plist from all users if present, became obsolete with version 33.0.0

doc/macOS-FinderSync-extension.md

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+
- SPDX-License-Identifier: GPL-2.0-or-later
4+
-->
5+
6+
# macOS Finder Integration (FinderSync) Extension
7+
8+
## Overview
9+
10+
The Finder integration — the Nextcloud context-menu entries and the sync-status
11+
icons (badges) shown for folders under Finder's **Favorites** — is provided by a
12+
separate macOS app extension, `FinderSyncExt` (bundle identifier
13+
`com.nextcloud.desktopclient.FinderSyncExt` for the official build). It is
14+
distinct from the File Provider extension (`FileProviderExt`), which drives the
15+
**Locations** integration for virtual files. macOS allows only one of the two to
16+
be active at a time, by design.
17+
18+
For the integration to work, three things must happen:
19+
20+
1. **Registration** — macOS (LaunchServices / PlugInKit, `pluginkit`) must
21+
discover and elect the extension for the logged-in user.
22+
2. **Enablement** — the user must have it enabled (this is the state a `+` in
23+
`pluginkit -m` reflects).
24+
3. **Connection** — the extension process, launched by Finder, must connect back
25+
to the running desktop client over XPC and complete a handshake.
26+
27+
Historically the integration appeared "not loaded" (missing badges/menus after a
28+
reboot) even when steps 1 and 2 were fine, because step 3 could silently fail —
29+
see [#10032](https://github.com/nextcloud/desktop/issues/10032),
30+
[#8471](https://github.com/nextcloud/desktop/issues/8471) and
31+
[#8363](https://github.com/nextcloud/desktop/issues/8363). The extension now only
32+
reports itself connected after a real XPC **handshake** round-trip with the
33+
client succeeds, and both the installer and the runtime log every outcome so a
34+
failure is recognizable from logs alone. This document explains how to read them.
35+
36+
## Verifying installation
37+
38+
The installer's postinstall script ([`admin/osx/post_install.sh.cmake`](../admin/osx/post_install.sh.cmake))
39+
registers and enables the extension **for the logged-in console user**
40+
(`pluginkit` state is per-user, and the script itself runs as `root`, so it hops
41+
into the user's context with `launchctl asuser` + `sudo -u`). Every step is
42+
logged to `/var/log/install.log` with the prefix `Nextcloud FinderSync:`.
43+
44+
```console
45+
grep "Nextcloud FinderSync" /var/log/install.log
46+
```
47+
48+
A healthy installation ends with:
49+
50+
```
51+
Nextcloud FinderSync: pluginkit -e use succeeded for com.nextcloud.desktopclient.FinderSyncExt
52+
Nextcloud FinderSync: extension present in pluginkit database after registration (see line above for election state)
53+
```
54+
55+
Signals that registration did **not** happen:
56+
57+
| Log line | Meaning |
58+
| --- | --- |
59+
| `WARNING pluginkit -a failed …` / `WARNING pluginkit -e use failed …` | The registration/election command returned non-zero. |
60+
| `WARNING extension NOT present in pluginkit database after registration …` | The extension is still not registered after the attempt. |
61+
| `no console user logged in; skipping pluginkit registration …` | Installed with no GUI user (e.g. MDM/remote). The extension is expected to register on the user's first launch of the app instead. |
62+
| `pluginkit not found; cannot register extension …` | `pluginkit` is unavailable on this system. |
63+
64+
## Verifying at runtime
65+
66+
### Is the extension registered and enabled?
67+
68+
```console
69+
pluginkit -mvvv -i com.nextcloud.desktopclient.FinderSyncExt
70+
```
71+
72+
A leading `+` means enabled, `-` means installed but disabled, `?` means
73+
registered without an explicit election, and empty output means it is not
74+
registered at all.
75+
76+
### Does the extension reach the client? (the handshake)
77+
78+
The extension logs its connection lifecycle to the unified system log under its
79+
own bundle identifier as subsystem. Watch it live while the client starts (or
80+
right after logging in):
81+
82+
```console
83+
log stream --level info --predicate 'subsystem == "com.nextcloud.desktopclient.FinderSyncExt"'
84+
```
85+
86+
Or inspect what already happened (info-level entries are not persisted forever,
87+
so keep the window short):
88+
89+
```console
90+
log show --last 10m --info --predicate 'subsystem == "com.nextcloud.desktopclient.FinderSyncExt"'
91+
```
92+
93+
Key messages and what they mean:
94+
95+
| Message | Level | Meaning |
96+
| --- | --- | --- |
97+
| `FinderSync XPC handshake succeeded; connection to app is live` | info | **Working.** The extension confirmed the client is on the other end. |
98+
| `Performing XPC handshake with app (generation N)` | info | A connection attempt is in progress. |
99+
| `FinderSync XPC handshake timed out after 5 s` | error | The client was reachable but did not answer the handshake. A reconnect is scheduled. |
100+
| `FinderSync XPC message to app failed: …` | error | The client is not listening yet (e.g. extension started before the app). A reconnect is scheduled. |
101+
| `FinderSync XPC connection lost (<reason>); scheduling reconnect` | error | An established connection dropped (client quit, interrupted, …). |
102+
| `Scheduling reconnect in N seconds` | info | Backoff between retries (1 → 2 → 4 → 8 s). |
103+
104+
Transient failures immediately after login are normal: the extension is
105+
typically launched by Finder before the client finishes starting, so you may see
106+
a `message to app failed` / reconnect or two followed by `handshake succeeded`.
107+
What indicates a real problem is `handshake succeeded` **never** appearing, or a
108+
previously-live connection producing repeated `connection lost` entries.
109+
110+
The client side of the same handshake is logged in the **desktop client log**
111+
(not the system log), at info level under the categories
112+
`nextcloud.gui.macos.findersync.xpc` and `nextcloud.gui.macfindersyncservice`.
113+
The positive confirmation there is:
114+
115+
```
116+
FinderSync extension handshake received; connection to app is live
117+
```
118+
119+
## Remediation
120+
121+
If the extension is registered and enabled (`+` above) but never completes the
122+
handshake, and the client is definitely running:
123+
124+
1. Disable and re-enable **Nextcloud** in **System Settings** under **Login Items
125+
& Extensions** (on older macOS: **Privacy & Security ▸ Extensions**).
126+
2. Relaunch Finder (`killall Finder`).
127+
128+
These are workarounds for a stuck state, not a fix. When reporting a problem,
129+
attach the output of the `grep`, `pluginkit -m` and `log show` commands above.
130+
131+
## Implementation references
132+
133+
- Extension XPC client and handshake / reconnect logic:
134+
[`shell_integration/MacOSX/NextcloudIntegration/FinderSyncExt/FinderSyncXPCManager.m`](../shell_integration/MacOSX/NextcloudIntegration/FinderSyncExt/FinderSyncXPCManager.m)
135+
- Extension principal object (badges, menus, bounded menu wait):
136+
[`shell_integration/MacOSX/NextcloudIntegration/FinderSyncExt/FinderSync.m`](../shell_integration/MacOSX/NextcloudIntegration/FinderSyncExt/FinderSync.m)
137+
- Shared XPC protocol (`performHandshakeWithReply:`):
138+
[`shell_integration/MacOSX/NextcloudIntegration/FinderSyncExt/Services/FinderSyncAppProtocol.h`](../shell_integration/MacOSX/NextcloudIntegration/FinderSyncExt/Services/FinderSyncAppProtocol.h)
139+
- Client-side XPC listener and service:
140+
[`src/gui/macOS/findersyncxpc_mac.mm`](../src/gui/macOS/findersyncxpc_mac.mm),
141+
[`src/gui/macOS/findersyncservice.mm`](../src/gui/macOS/findersyncservice.mm)
142+
- Installer registration:
143+
[`admin/osx/post_install.sh.cmake`](../admin/osx/post_install.sh.cmake)

doc/macOS-development.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,4 +179,5 @@ The way Transifex handles Xcode string catalogs creates a high risk of accidenta
179179

180180
- **Direct `mac-crafter` CLI usage / branding builds**[`admin/osx/mac-crafter/README.md`](../admin/osx/mac-crafter/README.md)
181181
- **Qt + macOS App Sandbox internals**[`doc/macOS-Sandbox-Qt.md`](./macOS-Sandbox-Qt.md)
182+
- **Finder integration (FinderSync) extension — verifying & troubleshooting loading**[`doc/macOS-FinderSync-extension.md`](./macOS-FinderSync-extension.md)
182183
- **NextcloudFileProviderKit Swift package**[`shell_integration/MacOSX/NextcloudFileProviderKit/README.md`](../shell_integration/MacOSX/NextcloudFileProviderKit/README.md)

shell_integration/MacOSX/NextcloudIntegration/FinderSyncExt/FinderSync.m

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,16 @@ @interface FinderSync()
1515
NSMutableDictionary *_strings;
1616
NSMutableArray *_menuItems;
1717
NSCondition *_menuIsComplete;
18+
BOOL _menuReady; // Predicate for _menuIsComplete; guards against lost NSCondition signals.
1819
os_log_t _log;
1920
}
2021
@end
2122

23+
// Upper bound on how long -menuForMenuKind: blocks Finder's thread waiting for the app to
24+
// return menu items. A healthy connection replies in milliseconds; this only bites when the
25+
// app died mid-request, so we cap it rather than hang Finder indefinitely (issues #10032/#8363).
26+
static const NSTimeInterval kFinderSyncMenuWaitTimeoutSeconds = 5.0;
27+
2228
static os_log_t getFinderSyncLogger(void) {
2329
static dispatch_once_t onceToken;
2430
static os_log_t logger = NULL;
@@ -61,11 +67,12 @@ - (instancetype)init
6167
[syncController setBadgeImage:warning label:@"Ignored" forBadgeIdentifier:@"IGNORE+SWM"];
6268
[syncController setBadgeImage:error label:@"Error" forBadgeIdentifier:@"ERROR+SWM"];
6369

64-
// Initialize XPC manager instead of socket client
70+
// Initialize XPC manager instead of socket client. The manager pulls the localized
71+
// strings itself once the handshake with the app succeeds, so there is no point
72+
// requesting them here — at init time the connection is never established yet.
6573
os_log_info(_log, "Initializing FinderSync XPC manager");
6674
self.xpcManager = [[FinderSyncXPCManager alloc] initWithDelegate:self];
6775
[self.xpcManager start];
68-
[self.xpcManager askOnSocket:@"" query:@"GET_STRINGS"];
6976

7077
_registeredDirectories = NSMutableSet.set;
7178
_strings = NSMutableDictionary.dictionary;
@@ -118,10 +125,20 @@ - (NSString*) selectedPathsSeparatedByRecordSeparator
118125
- (void)waitForMenuToArrive
119126
{
120127
os_log_debug(_log, "Waiting for menu to arrive");
128+
NSDate *const deadline = [NSDate dateWithTimeIntervalSinceNow:kFinderSyncMenuWaitTimeoutSeconds];
121129
[self->_menuIsComplete lock];
122-
[self->_menuIsComplete wait];
130+
// Loop on a predicate rather than a bare -wait: an NSCondition signal is not sticky, so if
131+
// -menuHasCompleted fired before we reached -wait the signal would be lost and we would block
132+
// forever. The deadline additionally guarantees we never hang Finder if the reply never comes.
133+
while (!self->_menuReady) {
134+
if (![self->_menuIsComplete waitUntilDate:deadline]) {
135+
os_log_error(_log, "Timed out waiting for menu items from app after %.0f s; returning what we have",
136+
(double)kFinderSyncMenuWaitTimeoutSeconds);
137+
break;
138+
}
139+
}
123140
[self->_menuIsComplete unlock];
124-
os_log_debug(_log, "Menu arrival wait completed");
141+
os_log_debug(_log, "Menu arrival wait completed (ready=%d)", self->_menuReady);
125142
}
126143

127144
- (NSMenu *)menuForMenuKind:(FIMenuKind)whichMenu
@@ -156,6 +173,13 @@ - (NSMenu *)menuForMenuKind:(FIMenuKind)whichMenu
156173
os_log_debug(_log, "Root directories check: onlyRootsSelected = %d", onlyRootsSelected);
157174

158175
NSString *paths = [self selectedPathsSeparatedByRecordSeparator];
176+
177+
// Arm the predicate before issuing the async request so a reply that races back
178+
// before -waitForMenuToArrive cannot be missed.
179+
[self->_menuIsComplete lock];
180+
self->_menuReady = NO;
181+
[self->_menuIsComplete unlock];
182+
159183
[self.xpcManager askOnSocket:paths query:@"GET_MENU_ITEMS"];
160184

161185
// Since the XPC communication is asynchronous, wait here until the menu
@@ -271,7 +295,10 @@ - (void)addMenuItem:(NSDictionary *)item {
271295
- (void)menuHasCompleted
272296
{
273297
os_log_debug(_log, "Menu completion signal received");
298+
[self->_menuIsComplete lock];
299+
self->_menuReady = YES;
274300
[self->_menuIsComplete signal];
301+
[self->_menuIsComplete unlock];
275302
os_log_debug(_log, "Menu signal emitted");
276303
}
277304

0 commit comments

Comments
 (0)