release: v1.0.07 (build B1006)
This commit is contained in:
1 parent
dd46bd9807
commit
e029f0bb17
9 files changed
+258
-2
No files matched your search
@@ -85,6 +85,11 @@ let package = Package(
|
||||
dependencies: ["OpenLessPolish"],
|
||||
path: "Tests/OpenLessPolishTests"
|
||||
),
|
||||
.testTarget(
|
||||
name: "OpenLessAppTests",
|
||||
dependencies: ["OpenLessApp"],
|
||||
path: "Tests/OpenLessAppTests"
|
||||
),
|
||||
],
|
||||
// 暂留 Swift 5 语言模式,避免一次性吞掉 Swift 6 严格并发的全部改造。
|
||||
// 升 .v6 时需要重新审视 Recorder / VolcengineStreamingASR 等 @unchecked Sendable 类。
|
||||
|
||||
@@ -11,6 +11,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
private var updaterController: UpdaterController?
|
||||
|
||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||
VersionedPermissionRefresh().resetIfNeeded()
|
||||
|
||||
// 不依赖 UserPreferences flag:每次启动直接查实际权限。
|
||||
// ad-hoc 签名下每次 rebuild 二进制 hash 都变,TCC 会自动失效,
|
||||
// 必须在那种情况下重新进引导,否则用户看不到 menu bar 也就罢了,
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import Foundation
|
||||
|
||||
struct TCCPermissionResetter {
|
||||
static let services = [
|
||||
"Accessibility",
|
||||
"Microphone",
|
||||
"AppleEvents",
|
||||
"ListenEvent",
|
||||
]
|
||||
|
||||
let bundleIdentifier: String
|
||||
let resetService: (String, String) -> Void
|
||||
|
||||
init(
|
||||
bundleIdentifier: String = Bundle.main.bundleIdentifier ?? "com.openless.app",
|
||||
resetService: @escaping (String, String) -> Void = TCCPermissionResetter.runTCCUtilReset
|
||||
) {
|
||||
self.bundleIdentifier = bundleIdentifier
|
||||
self.resetService = resetService
|
||||
}
|
||||
|
||||
func resetAll() {
|
||||
for service in Self.services {
|
||||
resetService(service, bundleIdentifier)
|
||||
}
|
||||
}
|
||||
|
||||
private static func runTCCUtilReset(service: String, bundleIdentifier: String) {
|
||||
let process = Process()
|
||||
process.executableURL = URL(fileURLWithPath: "/usr/bin/tccutil")
|
||||
process.arguments = ["reset", service, bundleIdentifier]
|
||||
|
||||
do {
|
||||
try process.run()
|
||||
process.waitUntilExit()
|
||||
|
||||
if process.terminationStatus == 0 {
|
||||
Log.write("[permissions] reset TCC \(service) for \(bundleIdentifier)")
|
||||
} else {
|
||||
Log.write("[permissions] tccutil reset \(service) for \(bundleIdentifier) exited with \(process.terminationStatus)")
|
||||
}
|
||||
} catch {
|
||||
Log.write("[permissions] failed to run tccutil for \(service): \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import Foundation
|
||||
|
||||
struct VersionedPermissionRefresh {
|
||||
static let lastResetVersionKey = "openless.permissions.last_tcc_reset_version"
|
||||
|
||||
let currentVersion: String
|
||||
let defaults: UserDefaults
|
||||
let resetter: TCCPermissionResetter
|
||||
let isRunningFromAppBundle: Bool
|
||||
|
||||
init(
|
||||
currentVersion: String = VersionedPermissionRefresh.currentBundleVersion(),
|
||||
defaults: UserDefaults = .standard,
|
||||
resetter: TCCPermissionResetter = TCCPermissionResetter(),
|
||||
isRunningFromAppBundle: Bool = VersionedPermissionRefresh.isRunningFromAppBundle()
|
||||
) {
|
||||
self.currentVersion = currentVersion
|
||||
self.defaults = defaults
|
||||
self.resetter = resetter
|
||||
self.isRunningFromAppBundle = isRunningFromAppBundle
|
||||
}
|
||||
|
||||
func resetIfNeeded() {
|
||||
guard isRunningFromAppBundle else {
|
||||
Log.write("[permissions] skip versioned TCC reset outside app bundle")
|
||||
return
|
||||
}
|
||||
|
||||
guard defaults.string(forKey: Self.lastResetVersionKey) != currentVersion else {
|
||||
return
|
||||
}
|
||||
|
||||
Log.write("[permissions] bundle version changed; clearing old TCC approvals before permission onboarding")
|
||||
resetter.resetAll()
|
||||
defaults.set(currentVersion, forKey: Self.lastResetVersionKey)
|
||||
}
|
||||
|
||||
static func currentBundleVersion(bundle: Bundle = .main) -> String {
|
||||
let info = bundle.infoDictionary
|
||||
let shortVersion = info?["CFBundleShortVersionString"] as? String
|
||||
let buildNumber = info?["CFBundleVersion"] as? String
|
||||
|
||||
switch (shortVersion, buildNumber) {
|
||||
case let (.some(shortVersion), .some(buildNumber)):
|
||||
return "\(shortVersion)#\(buildNumber)"
|
||||
case let (.some(shortVersion), .none):
|
||||
return shortVersion
|
||||
case let (.none, .some(buildNumber)):
|
||||
return buildNumber
|
||||
case (.none, .none):
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
static func isRunningFromAppBundle(bundle: Bundle = .main) -> Bool {
|
||||
bundle.bundleURL.pathExtension == "app"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import XCTest
|
||||
@testable import OpenLessApp
|
||||
|
||||
final class TCCPermissionResetterTests: XCTestCase {
|
||||
func test_resetAllClearsEveryTCCServiceForBundleIdentifier() {
|
||||
var resetCalls: [(service: String, bundleIdentifier: String)] = []
|
||||
let resetter = TCCPermissionResetter(
|
||||
bundleIdentifier: "com.example.openless",
|
||||
resetService: { service, bundleIdentifier in
|
||||
resetCalls.append((service, bundleIdentifier))
|
||||
}
|
||||
)
|
||||
|
||||
resetter.resetAll()
|
||||
|
||||
XCTAssertEqual(resetCalls.map(\.service), [
|
||||
"Accessibility",
|
||||
"Microphone",
|
||||
"AppleEvents",
|
||||
"ListenEvent",
|
||||
])
|
||||
XCTAssertEqual(resetCalls.map(\.bundleIdentifier), Array(repeating: "com.example.openless", count: 4))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import XCTest
|
||||
@testable import OpenLessApp
|
||||
|
||||
final class VersionedPermissionRefreshTests: XCTestCase {
|
||||
func test_resetIfNeededClearsPermissionsAndStoresCurrentVersionWhenVersionChanged() {
|
||||
let defaults = makeDefaults()
|
||||
defaults.set("1.0.0#1", forKey: VersionedPermissionRefresh.lastResetVersionKey)
|
||||
var didReset = false
|
||||
|
||||
let refresh = VersionedPermissionRefresh(
|
||||
currentVersion: "1.0.1#2",
|
||||
defaults: defaults,
|
||||
resetter: TCCPermissionResetter(resetService: { _, _ in didReset = true }),
|
||||
isRunningFromAppBundle: true
|
||||
)
|
||||
|
||||
refresh.resetIfNeeded()
|
||||
|
||||
XCTAssertTrue(didReset)
|
||||
XCTAssertEqual(defaults.string(forKey: VersionedPermissionRefresh.lastResetVersionKey), "1.0.1#2")
|
||||
}
|
||||
|
||||
func test_resetIfNeededDoesNothingWhenVersionWasAlreadyHandled() {
|
||||
let defaults = makeDefaults()
|
||||
defaults.set("1.0.1#2", forKey: VersionedPermissionRefresh.lastResetVersionKey)
|
||||
var resetCount = 0
|
||||
|
||||
let refresh = VersionedPermissionRefresh(
|
||||
currentVersion: "1.0.1#2",
|
||||
defaults: defaults,
|
||||
resetter: TCCPermissionResetter(resetService: { _, _ in resetCount += 1 }),
|
||||
isRunningFromAppBundle: true
|
||||
)
|
||||
|
||||
refresh.resetIfNeeded()
|
||||
|
||||
XCTAssertEqual(resetCount, 0)
|
||||
XCTAssertEqual(defaults.string(forKey: VersionedPermissionRefresh.lastResetVersionKey), "1.0.1#2")
|
||||
}
|
||||
|
||||
func test_resetIfNeededSkipsWhenNotRunningFromAppBundle() {
|
||||
let defaults = makeDefaults()
|
||||
var resetCount = 0
|
||||
|
||||
let refresh = VersionedPermissionRefresh(
|
||||
currentVersion: "1.0.1#2",
|
||||
defaults: defaults,
|
||||
resetter: TCCPermissionResetter(resetService: { _, _ in resetCount += 1 }),
|
||||
isRunningFromAppBundle: false
|
||||
)
|
||||
|
||||
refresh.resetIfNeeded()
|
||||
|
||||
XCTAssertEqual(resetCount, 0)
|
||||
XCTAssertNil(defaults.string(forKey: VersionedPermissionRefresh.lastResetVersionKey))
|
||||
}
|
||||
|
||||
private func makeDefaults() -> UserDefaults {
|
||||
let suiteName = "OpenLessAppTests-\(UUID().uuidString)"
|
||||
let defaults = UserDefaults(suiteName: suiteName)!
|
||||
defaults.removePersistentDomain(forName: suiteName)
|
||||
return defaults
|
||||
}
|
||||
}
|
||||
+35
@@ -5,6 +5,41 @@
|
||||
<link>https://raw.githubusercontent.com/appergb/openless/main/appcast.xml</link>
|
||||
<description>OpenLess update feed</description>
|
||||
|
||||
<item>
|
||||
<title>OpenLess 1.0.07</title>
|
||||
<pubDate>Tue, 28 Apr 2026 09:36:07 +0000</pubDate>
|
||||
<sparkle:version>B1006</sparkle:version>
|
||||
<sparkle:shortVersionString>1.0.07</sparkle:shortVersionString>
|
||||
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
|
||||
<description>< 下载 zip,解压拖到 `/Applications`,跑一次:
|
||||
|
||||
```bash
|
||||
xattr -dr com.apple.quarantine /Applications/OpenLess.app
|
||||
```
|
||||
|
||||
之后双击启动。]]></description>
|
||||
<enclosure
|
||||
url="https://github.com/appergb/openless/releases/download/v1.0.07/OpenLess-1.0.07.zip"
|
||||
length="2803125"
|
||||
type="application/octet-stream"
|
||||
sparkle:edSignature="hgh2Bvjq0vYGYCgEhDm99wWgN0rnL12eKn3NwFRIY91Gb8nOdxelRcbSFXwj8CfIllHxECpbyJ3Yokvel889AQ==" />
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<title>OpenLess 1.0.06</title>
|
||||
<pubDate>Tue, 28 Apr 2026 05:36:01 +0000</pubDate>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# OpenLess 1.0.07
|
||||
|
||||
build B1006 — 修复更新后权限不会重新进入授权流程的问题。
|
||||
|
||||
## 修复
|
||||
|
||||
- **新版本安装后自动清理旧权限**。OpenLess 从 `.app` bundle 启动时会比较当前版本和上次已处理版本;发现新版本后,先自动清理旧的 Accessibility、Microphone、AppleEvents、ListenEvent TCC 授权记录。
|
||||
- **清理后自动进入权限请求流程**。旧权限清掉后,启动流程继续按实际授权状态判断;缺少辅助功能或麦克风权限时,会直接显示现有授权引导窗口,让用户为新版本重新授权。
|
||||
- **同一版本不重复清理**。授权完成后再次打开同一版本不会反复 reset;下一个版本安装后会再次执行同样逻辑。
|
||||
- **开发运行更安全**。只有从 `.app` bundle 启动时才触发版本化权限清理,避免 `swift run` 等开发调试误清理已安装应用的权限。
|
||||
|
||||
## 安装
|
||||
|
||||
**老用户**:启动 OpenLess 后自动检查更新。安装新版本并重新打开后,会按新版本权限流程重新授权。
|
||||
|
||||
**新用户**:到 [Releases](https://github.com/appergb/openless/releases) 下载 zip,解压拖到 `/Applications`,跑一次:
|
||||
|
||||
```bash
|
||||
xattr -dr com.apple.quarantine /Applications/OpenLess.app
|
||||
```
|
||||
|
||||
之后双击启动。
|
||||
@@ -6,8 +6,8 @@ cd "$(dirname "$0")/.."
|
||||
|
||||
APP_NAME="OpenLess"
|
||||
BUNDLE_ID="com.openless.app"
|
||||
APP_VERSION="1.0.06"
|
||||
BUILD_NUMBER="B1005"
|
||||
APP_VERSION="1.0.07"
|
||||
BUILD_NUMBER="B1006"
|
||||
BUILD_DIR="build"
|
||||
APP_DIR="${BUILD_DIR}/${APP_NAME}.app"
|
||||
BIN_DIR="${APP_DIR}/Contents/MacOS"
|
||||
|
||||
Reference in new issue
Block a user