diff --git a/Sources/SessionPersistence.swift b/Sources/SessionPersistence.swift index b0303d53..188833d4 100644 --- a/Sources/SessionPersistence.swift +++ b/Sources/SessionPersistence.swift @@ -377,9 +377,10 @@ enum SessionPersistenceStore { let directory = fileURL.deletingLastPathComponent() do { try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true, attributes: nil) - let encoder = JSONEncoder() - encoder.outputFormatting = [.sortedKeys] - let data = try encoder.encode(snapshot) + let data = try encodedSnapshotData(snapshot) + if let existingData = try? Data(contentsOf: fileURL), existingData == data { + return true + } try data.write(to: fileURL, options: .atomic) return true } catch { @@ -387,6 +388,12 @@ enum SessionPersistenceStore { } } + private static func encodedSnapshotData(_ snapshot: AppSessionSnapshot) throws -> Data { + let encoder = JSONEncoder() + encoder.outputFormatting = [.sortedKeys] + return try encoder.encode(snapshot) + } + static func removeSnapshot(fileURL: URL? = nil) { guard let fileURL = fileURL ?? defaultSnapshotFileURL() else { return } try? FileManager.default.removeItem(at: fileURL) diff --git a/cmuxTests/SessionPersistenceTests.swift b/cmuxTests/SessionPersistenceTests.swift index 7d04db1d..8c00c0c1 100644 --- a/cmuxTests/SessionPersistenceTests.swift +++ b/cmuxTests/SessionPersistenceTests.swift @@ -86,6 +86,28 @@ final class SessionPersistenceTests: XCTestCase { ) } + func testSaveSkipsRewritingIdenticalSnapshotData() throws { + let tempDir = FileManager.default.temporaryDirectory + .appendingPathComponent("cmux-session-tests-\(UUID().uuidString)", isDirectory: true) + try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: tempDir) } + + let snapshotURL = tempDir.appendingPathComponent("session.json", isDirectory: false) + let snapshot = makeSnapshot(version: SessionSnapshotSchema.currentVersion) + + XCTAssertTrue(SessionPersistenceStore.save(snapshot, fileURL: snapshotURL)) + let firstFileNumber = try fileNumber(for: snapshotURL) + + XCTAssertTrue(SessionPersistenceStore.save(snapshot, fileURL: snapshotURL)) + let secondFileNumber = try fileNumber(for: snapshotURL) + + XCTAssertEqual( + secondFileNumber, + firstFileNumber, + "Saving identical session data should not replace the snapshot file" + ) + } + func testWorkspaceCustomColorDecodeSupportsMissingLegacyField() throws { var snapshot = makeSnapshot(version: SessionSnapshotSchema.currentVersion) snapshot.windows[0].tabManager.workspaces[0].customColor = nil @@ -780,6 +802,11 @@ final class SessionPersistenceTests: XCTestCase { windows: [window] ) } + + private func fileNumber(for fileURL: URL) throws -> Int { + let attributes = try FileManager.default.attributesOfItem(atPath: fileURL.path) + return try XCTUnwrap(attributes[.systemFileNumber] as? Int) + } } final class SocketListenerAcceptPolicyTests: XCTestCase {