diff --git a/Sources/MacClean/Core/Scanner/TargetedScanner.swift b/Sources/MacClean/Core/Scanner/TargetedScanner.swift index c9dd85d..cb6f767 100644 --- a/Sources/MacClean/Core/Scanner/TargetedScanner.swift +++ b/Sources/MacClean/Core/Scanner/TargetedScanner.swift @@ -192,8 +192,13 @@ public actor TargetedScanner { private static func makeFileItem(from url: URL, keys: [URLResourceKey]) -> FileItem? { guard let values = try? url.resourceValues(forKeys: Set(keys)) else { return nil } + // (st_dev, st_ino) let DuplicateDetection collapse hard links. If lstat + // fails (transient permission/race), keep the file with inode/deviceID 0 + // rather than dropping it from the scan: 0 is treated as "unknown, never + // a hard-link match", so the file still shows up for the six non-dedup + // modules and is simply never collapsed as a duplicate. var metadata = stat() - guard lstat(url.path(percentEncoded: false), &metadata) == 0 else { return nil } + let hasIdentity = lstat(url.path(percentEncoded: false), &metadata) == 0 return FileItem( url: url, @@ -206,8 +211,8 @@ public actor TargetedScanner { contentType: values.contentType, creationDate: values.creationDate, modificationDate: values.contentModificationDate, - inode: UInt64(metadata.st_ino), - deviceID: Int32(metadata.st_dev) + inode: hasIdentity ? UInt64(metadata.st_ino) : 0, + deviceID: hasIdentity ? Int32(metadata.st_dev) : 0 ) } } diff --git a/Sources/MacClean/Views/Performance/ResourceHogsView.swift b/Sources/MacClean/Views/Performance/ResourceHogsView.swift index 74a0570..dab814d 100644 --- a/Sources/MacClean/Views/Performance/ResourceHogsView.swift +++ b/Sources/MacClean/Views/Performance/ResourceHogsView.swift @@ -141,6 +141,21 @@ struct ResourceHogsView: View { showStatus = true return } + // Guard against PID reuse: the row was captured from an earlier + // snapshot. Re-resolve the live process and confirm it is still the + // same app (and still quittable) before terminating, so a recycled PID + // can never take down a different app. + guard let live = NSRunningApplication(processIdentifier: row.snapshot.pid), + live.bundleIdentifier == row.snapshot.bundleIdentifier, + ResourceHogsPolicy.canQuit(bundleIdentifier: live.bundleIdentifier) else { + statusMessage = L10n.tr( + "该进程已不再运行。", + "That process is no longer running.", + "Этот процесс больше не запущен." + ) + showStatus = true + return + } monitor.forceQuit(pid: row.snapshot.pid) Task { await refresh() } } diff --git a/Sources/MacCleanKit/Constants.swift b/Sources/MacCleanKit/Constants.swift index 99faafe..733adc1 100644 --- a/Sources/MacCleanKit/Constants.swift +++ b/Sources/MacCleanKit/Constants.swift @@ -223,5 +223,5 @@ public enum MCConstants { // plugin was tried (commit history) but doesn't work under multi-arch // `swift build --arch arm64 --arch x86_64` because xcbuild doesn't // execute plugins. - public static let appVersion = "1.20.0" + public static let appVersion = "1.21.0" } diff --git a/VERSION b/VERSION index 3989355..3500250 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.20.0 +1.21.0