Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ All notable changes to RClick are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/).

## [2.3.0] - 2026-09-16

### Changed
- Builds clean against the macOS 27 SDK with Xcode 27, with no compiler warnings

### Fixed
- RClick quit instantly and silently whenever it could not open its configuration database. It now reports the cause and exits cleanly instead of trapping. The case seen in the wild is a build that is not properly code-signed, which cannot reach the shared App Group container

## [2.2.1] - 2026-09-13

### Added
Expand Down
15 changes: 11 additions & 4 deletions FinderSyncExt/FinderSyncExt.swift
Original file line number Diff line number Diff line change
Expand Up @@ -236,11 +236,18 @@ class FinderSyncExt: FIFinderSync, @unchecked Sendable {
logger.debug("endObservingDirectoryAtURL: \(url.path)")
}

/// Finder 会为观察范围内的每个项目回调这里,问它该显示什么徽章。
///
/// RClick 不使用徽章,所以留空:扩展包里没有任何 badge 图(FinderSync 只认
/// Resources 下的 `<id>.png` 散文件,不认 asset catalog),没有可设的标识。
///
/// ⚠️ 这两行**不是**"外置盘图标变成 RClick 图标"的修复。#155 那个现象在这里
/// 被误判过两次:25b69e8 把随机标识改成空串,b697cc3 改成干脆不调用 —— 两次
/// 都没有解决,第二次还实测过"不调用时图标照旧"。真正的原因在别处:扩展把
/// 每个卷根都放进了 `directoryURLs`(见 setupObservingDirectories),Finder
/// 因而重新渲染这些卷的侧边栏图标。别再往 badge 上找了。
override func requestBadgeIdentifier(for url: URL) {
// 不设置任何徽章标识,避免 Finder 在项目上叠加 RClick 图标。
// 非空徽章 ID 会使 Finder 在文件/磁盘图标上显示扩展的图标叠加层,
// 这会导致移动磁盘和光盘等外部卷的图标被 RClick 图标覆盖。
FIFinderSyncController.default().setBadgeIdentifier("", for: url)
// 故意留空,见上。
}

// MARK: - Menu and toolbar item support
Expand Down
16 changes: 8 additions & 8 deletions RClick.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -489,7 +489,7 @@
CODE_SIGN_ENTITLEMENTS = RClick/RClickDebug.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 20260913002;
CURRENT_PROJECT_VERSION = 20260916001;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 4L3563XCBN;
ENABLE_APP_SANDBOX = YES;
Expand All @@ -504,7 +504,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 15.6;
MARKETING_VERSION = 2.2.1;
MARKETING_VERSION = 2.3.0;
PRODUCT_BUNDLE_IDENTIFIER = cn.wflixu.RClick;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
Expand All @@ -526,7 +526,7 @@
CODE_SIGN_ENTITLEMENTS = RClick/RClick.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 20260913002;
CURRENT_PROJECT_VERSION = 20260916001;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 4L3563XCBN;
ENABLE_APP_SANDBOX = YES;
Expand All @@ -541,7 +541,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 15.6;
MARKETING_VERSION = 2.2.1;
MARKETING_VERSION = 2.3.0;
PRODUCT_BUNDLE_IDENTIFIER = cn.wflixu.RClick;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
Expand Down Expand Up @@ -604,7 +604,7 @@
CODE_SIGN_ENTITLEMENTS = FinderSyncExt/FinderSyncExt.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 20260913002;
CURRENT_PROJECT_VERSION = 20260916001;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 4L3563XCBN;
ENABLE_APP_SANDBOX = YES;
Expand All @@ -620,7 +620,7 @@
"@executable_path/../../../../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 15.6;
MARKETING_VERSION = 2.2.1;
MARKETING_VERSION = 2.3.0;
PRODUCT_BUNDLE_IDENTIFIER = cn.wflixu.RClick.FinderSyncExt;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
Expand All @@ -639,7 +639,7 @@
CODE_SIGN_ENTITLEMENTS = FinderSyncExt/FinderSyncExt.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 20260913002;
CURRENT_PROJECT_VERSION = 20260916001;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 4L3563XCBN;
ENABLE_APP_SANDBOX = YES;
Expand All @@ -655,7 +655,7 @@
"@executable_path/../../../../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 15.6;
MARKETING_VERSION = 2.2.1;
MARKETING_VERSION = 2.3.0;
PRODUCT_BUNDLE_IDENTIFIER = cn.wflixu.RClick.FinderSyncExt;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
Expand Down
105 changes: 105 additions & 0 deletions RClick/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -4477,6 +4477,41 @@
}
}
},
"RClick could not access its shared App Group container, so it cannot read its settings." : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "RClick could not access its shared App Group container, so it cannot read its settings."
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "RClick no pudo acceder a su contenedor compartido de App Group, por lo que no puede leer su configuración."
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "RClick n'a pas pu accéder à son conteneur App Group partagé et ne peut donc pas lire sa configuration."
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "RClick は共有 App Group コンテナにアクセスできないため、設定を読み込めません。"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "RClick 无法访问共享的 App Group 容器,因此读不到配置数据。"
}
}
}
},
"RClick is a right-click menu extension that allows you to add applications for opening folders and includes some common actions." : {
"extractionState" : "manual",
"localizations" : {
Expand Down Expand Up @@ -5667,6 +5702,41 @@
}
}
},
"The most likely cause is that this build is not properly signed. Rebuild it with a valid signing identity — a build signed to run locally cannot reach the App Group container." : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "The most likely cause is that this build is not properly signed. Rebuild it with a valid signing identity — a build signed to run locally cannot reach the App Group container."
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "La causa más probable es que esta compilación no está firmada correctamente. Vuelve a compilarla con una identidad de firma válida: una compilación firmada para ejecutarse localmente no puede acceder al contenedor de App Group."
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "La cause la plus probable est que cette version n'est pas correctement signée. Recompilez-la avec une identité de signature valide : une version signée pour s'exécuter localement ne peut pas accéder au conteneur App Group."
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "最も可能性が高い原因は、このビルドが正しく署名されていないことです。有効な署名 ID で再ビルドしてください。「ローカルで実行」用に署名されたビルドは App Group コンテナにアクセスできません。"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "最可能的原因是这个构建没有正确签名。请用有效的签名身份重新构建 —— 以「本地运行」方式签名的构建访问不了 App Group 容器。"
}
}
}
},
"The selected folder is a subfolder of an already selected folder. Please choose a different folder." : {
"extractionState" : "manual",
"localizations" : {
Expand Down Expand Up @@ -5912,6 +5982,41 @@
}
}
},
"Unable to open the configuration database" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Unable to open the configuration database"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "No se puede abrir la base de datos de configuración"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Impossible d'ouvrir la base de données de configuration"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "設定データベースを開けません"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "无法打开配置数据库"
}
}
}
},
"Unauthorized Folder" : {
"extractionState" : "manual",
"localizations" : {
Expand Down
6 changes: 5 additions & 1 deletion RClick/MenuBarView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,12 @@ struct MenuBarView: View {
private func actionQuit() {
messager.sendQuitNotification()

// 等一拍让菜单栏弹出面板收起,再退出。
//
// 这里唯一的抛出是任务被取消;用户点的是"退出",不该因为一次取消就被留在这里,
// 所以显式吞掉取消、继续走完 —— 而不是让错误悄悄丢掉(这条警告说的正是后者)。
Task { @MainActor in
try await Task.sleep(nanoseconds: UInt64(1.0 * 1e9))
try? await Task.sleep(for: .seconds(1))

NSApplication.shared.terminate(self)
}
Expand Down
102 changes: 83 additions & 19 deletions RClick/Model/ModelContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,42 @@ import Foundation
import SwiftData
import OSLog

/// 共享库打不开的原因。
///
/// 拆成两个 case 而不是笼统一个"失败",因为两者的排查路径完全不同:
/// 前者查 App Group 配置,后者多半是构建签名问题。
enum SharedStoreError: LocalizedError {
/// 拿不到 App Group 容器目录:App Group 没配好,或 entitlement 没生效。
case appGroupUnavailable(identifier: String)
/// 目录拿到了,但库文件打不开。
case storeUnreadable(url: URL, underlying: Error)

/// 诊断用,不是给用户看的文案 —— 用户看到的那份由 `StartupFailure.describe()`
/// 组装并走本地化。这里保持英文,避免一条没进 catalog 的中文串从某处
/// `localizedDescription` 漏到界面上。
var errorDescription: String? {
switch self {
case .appGroupUnavailable(let identifier):
return "Cannot resolve the App Group container: \(identifier)"
case .storeUnreadable(let url, let underlying):
return "Cannot open the store at \(url.path): \(underlying)"
}
}

/// 错误链里是否出现 SQLite 的授权被拒(SQLITE_AUTH = 23)。
///
/// 这是"库文件在、当前进程却不许读"的特征。已知的唯一成因是构建产物没有有效签名:
/// `com.apple.security.application-groups` 没有被 provisioning profile 背书,
/// 沙盒因此不把容器目录写进本进程的 profile,打开库时即被拒。
///
/// 之所以靠字符串而不是 `NSError.userInfo`:SwiftData 把 Cocoa 错误裹在
/// `SwiftDataError` 里,`NSSQLiteErrorDomain` 只出现在它的描述中,沿错误链取不到。
var looksLikeAuthorizationDenial: Bool {
guard case .storeUnreadable(_, let underlying) = self else { return false }
return "\(underlying)".contains("NSSQLiteErrorDomain=23")
}
}

// 共享 ModelContainer 配置工具类
@MainActor
class SharedDataManager {
Expand All @@ -19,25 +55,54 @@ class SharedDataManager {
category: "ModelContainer"
)

static var sharedModelContainer: ModelContainer = {
/// `bootstrap()` 打开的那个容器。
private static var openedContainer: ModelContainer?

/// 已打开的共享容器。
///
/// 正常路径由启动代码先跑 `bootstrap()`。若仍有更早的访问者(SwiftUI 求值顺序不是
/// 我们能保证的),这里补一次并走同一个失败出口 —— 总之不再有 trap。
static var sharedModelContainer: ModelContainer {
if let openedContainer { return openedContainer }

do {
// 获取 App Group 共享目录
let storeURL: URL
let container = try makeSharedModelContainer()
openedContainer = container
return container
} catch {
StartupFailure.presentAndExit(error)
}
}

guard let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier) else {
fatalError("无法获取 App Group 共享目录。请检查 App Group 配置: \(appGroupIdentifier)")
}
storeURL = containerURL.appendingPathComponent("RClickDatabase.sqlite")
/// 打开共享容器,失败则抛出。
///
/// 启动路径应当在任何东西碰 `sharedModelContainer` 之前调用它。原先是静态 lazy
/// 初始化 + `fatalError`,于是"读不到库"表现为毫无提示的 SIGTRAP —— 崩溃报告里
/// 只有 `_assertionFailure`,真正的原因(哪个文件、为什么打不开)随进程一起没了。
/// 改成显式抛出后,调用方能把原因说清楚再退。
static func bootstrap() throws {
guard openedContainer == nil else { return }
openedContainer = try makeSharedModelContainer()
}

// 创建 ModelConfiguration 使用共享路径
let configuration = ModelConfiguration(
url: storeURL,
allowsSave: true,
cloudKitDatabase: .none
)
/// 定位容器目录并打开 ModelContainer。不做任何 trap,失败一律抛给调用方。
static func makeSharedModelContainer() throws -> ModelContainer {
// 获取 App Group 共享目录
guard let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier) else {
throw SharedStoreError.appGroupUnavailable(identifier: appGroupIdentifier)
}
let storeURL = containerURL.appendingPathComponent("RClickDatabase.sqlite")

// 创建 ModelContainer,注册所有模型
let container = try ModelContainer(
// 创建 ModelConfiguration 使用共享路径
let configuration = ModelConfiguration(
url: storeURL,
allowsSave: true,
cloudKitDatabase: .none
)

// 创建 ModelContainer,注册所有模型
do {
return try ModelContainer(
for: AppEntity.self,
ActionEntity.self,
NewFileTypeEntity.self,
Expand All @@ -46,12 +111,11 @@ class SharedDataManager {
DataVersion.self,
configurations: configuration
)

return container
} catch {
fatalError("创建共享 ModelContainer 失败: \(error)")
logger.error("打开共享 ModelContainer 失败: \(error)")
throw SharedStoreError.storeUnreadable(url: storeURL, underlying: error)
}
}()
}

/// 初始化默认数据
static func initializeDefaultData(context: ModelContext) async {
Expand Down
Loading