Fix stale session geometry crash after 0.63.0 upgrade (#2306)
* Add regression coverage for stale window geometry migration * Discard stale persisted window geometry on launch
This commit is contained in:
parent
97c2bc92d4
commit
f1be3978ab
2 changed files with 89 additions and 4 deletions
|
|
@ -2042,12 +2042,17 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
let visibleFrame: CGRect
|
||||
}
|
||||
|
||||
private struct PersistedWindowGeometry: Codable, Sendable {
|
||||
struct PersistedWindowGeometry: Codable, Sendable {
|
||||
let version: Int
|
||||
let frame: SessionRectSnapshot
|
||||
let display: SessionDisplaySnapshot?
|
||||
}
|
||||
|
||||
private static let persistedWindowGeometryDefaultsKey = "cmux.session.lastWindowGeometry.v1"
|
||||
nonisolated static let persistedWindowGeometrySchemaVersion = 2
|
||||
private nonisolated static let persistedWindowGeometryDefaultsKey = "cmux.session.lastWindowGeometry.v2"
|
||||
private nonisolated static let legacyPersistedWindowGeometryDefaultsKeys = [
|
||||
"cmux.session.lastWindowGeometry.v1"
|
||||
]
|
||||
|
||||
weak var tabManager: TabManager?
|
||||
weak var notificationStore: TerminalNotificationStore?
|
||||
|
|
@ -2874,16 +2879,22 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
guard !didPrepareStartupSessionSnapshot else { return }
|
||||
didPrepareStartupSessionSnapshot = true
|
||||
guard SessionRestorePolicy.shouldAttemptRestore() else { return }
|
||||
Self.removeLegacyPersistedWindowGeometry()
|
||||
startupSessionSnapshot = SessionPersistenceStore.load()
|
||||
}
|
||||
|
||||
private func persistedWindowGeometry(
|
||||
defaults: UserDefaults = .standard
|
||||
) -> PersistedWindowGeometry? {
|
||||
Self.removeLegacyPersistedWindowGeometry(defaults: defaults)
|
||||
guard let data = defaults.data(forKey: Self.persistedWindowGeometryDefaultsKey) else {
|
||||
return nil
|
||||
}
|
||||
return try? JSONDecoder().decode(PersistedWindowGeometry.self, from: data)
|
||||
guard let payload = Self.decodedPersistedWindowGeometryData(data) else {
|
||||
defaults.removeObject(forKey: Self.persistedWindowGeometryDefaultsKey)
|
||||
return nil
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
private func persistWindowGeometry(
|
||||
|
|
@ -2891,6 +2902,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
display: SessionDisplaySnapshot?,
|
||||
defaults: UserDefaults = .standard
|
||||
) {
|
||||
Self.removeLegacyPersistedWindowGeometry(defaults: defaults)
|
||||
guard let data = Self.encodedPersistedWindowGeometryData(frame: frame, display: display) else {
|
||||
return
|
||||
}
|
||||
|
|
@ -2902,10 +2914,28 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
display: SessionDisplaySnapshot?
|
||||
) -> Data? {
|
||||
guard let frame else { return nil }
|
||||
let payload = PersistedWindowGeometry(frame: frame, display: display)
|
||||
let payload = PersistedWindowGeometry(
|
||||
version: persistedWindowGeometrySchemaVersion,
|
||||
frame: frame,
|
||||
display: display
|
||||
)
|
||||
return try? JSONEncoder().encode(payload)
|
||||
}
|
||||
|
||||
nonisolated static func decodedPersistedWindowGeometryData(_ data: Data) -> PersistedWindowGeometry? {
|
||||
guard let payload = try? JSONDecoder().decode(PersistedWindowGeometry.self, from: data),
|
||||
payload.version == persistedWindowGeometrySchemaVersion else {
|
||||
return nil
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
private nonisolated static func removeLegacyPersistedWindowGeometry(
|
||||
defaults: UserDefaults = .standard
|
||||
) {
|
||||
legacyPersistedWindowGeometryDefaultsKeys.forEach { defaults.removeObject(forKey: $0) }
|
||||
}
|
||||
|
||||
private func persistWindowGeometry(from window: NSWindow?) {
|
||||
guard let window else { return }
|
||||
persistWindowGeometry(
|
||||
|
|
@ -3765,6 +3795,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
guard snapshot != nil || removeWhenEmpty || persistedGeometryData != nil else { return }
|
||||
|
||||
let writeBlock = {
|
||||
Self.removeLegacyPersistedWindowGeometry()
|
||||
if let persistedGeometryData {
|
||||
UserDefaults.standard.set(
|
||||
persistedGeometryData,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,11 @@ import XCTest
|
|||
#endif
|
||||
|
||||
final class SessionPersistenceTests: XCTestCase {
|
||||
private struct LegacyPersistedWindowGeometry: Codable {
|
||||
let frame: SessionRectSnapshot
|
||||
let display: SessionDisplaySnapshot?
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testWorkspaceSessionSnapshotRestoresMarkdownPanel() throws {
|
||||
let root = FileManager.default.temporaryDirectory
|
||||
|
|
@ -641,6 +646,55 @@ final class SessionPersistenceTests: XCTestCase {
|
|||
XCTAssertEqual(restored.height, 700, accuracy: 0.001)
|
||||
}
|
||||
|
||||
func testDecodedPersistedWindowGeometryDataAcceptsCurrentSchema() throws {
|
||||
let data = try JSONEncoder().encode(
|
||||
AppDelegate.PersistedWindowGeometry(
|
||||
version: AppDelegate.persistedWindowGeometrySchemaVersion,
|
||||
frame: SessionRectSnapshot(x: 220, y: 160, width: 980, height: 700),
|
||||
display: SessionDisplaySnapshot(
|
||||
displayID: 1,
|
||||
frame: SessionRectSnapshot(x: 0, y: 0, width: 1_600, height: 1_000),
|
||||
visibleFrame: SessionRectSnapshot(x: 0, y: 0, width: 1_600, height: 1_000)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
let decoded = try XCTUnwrap(AppDelegate.decodedPersistedWindowGeometryData(data))
|
||||
XCTAssertEqual(decoded.version, AppDelegate.persistedWindowGeometrySchemaVersion)
|
||||
XCTAssertEqual(decoded.frame.x, 220, accuracy: 0.001)
|
||||
XCTAssertEqual(decoded.frame.y, 160, accuracy: 0.001)
|
||||
XCTAssertEqual(decoded.frame.width, 980, accuracy: 0.001)
|
||||
XCTAssertEqual(decoded.frame.height, 700, accuracy: 0.001)
|
||||
XCTAssertEqual(decoded.display?.displayID, 1)
|
||||
}
|
||||
|
||||
func testDecodedPersistedWindowGeometryDataRejectsLegacyUnversionedPayload() throws {
|
||||
let data = try JSONEncoder().encode(
|
||||
LegacyPersistedWindowGeometry(
|
||||
frame: SessionRectSnapshot(x: 180, y: 140, width: 900, height: 640),
|
||||
display: SessionDisplaySnapshot(
|
||||
displayID: 1,
|
||||
frame: SessionRectSnapshot(x: 0, y: 0, width: 1_600, height: 1_000),
|
||||
visibleFrame: SessionRectSnapshot(x: 0, y: 0, width: 1_600, height: 1_000)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
XCTAssertNil(AppDelegate.decodedPersistedWindowGeometryData(data))
|
||||
}
|
||||
|
||||
func testDecodedPersistedWindowGeometryDataRejectsDifferentSchemaVersion() throws {
|
||||
let data = try JSONEncoder().encode(
|
||||
AppDelegate.PersistedWindowGeometry(
|
||||
version: AppDelegate.persistedWindowGeometrySchemaVersion + 1,
|
||||
frame: SessionRectSnapshot(x: 220, y: 160, width: 980, height: 700),
|
||||
display: nil
|
||||
)
|
||||
)
|
||||
|
||||
XCTAssertNil(AppDelegate.decodedPersistedWindowGeometryData(data))
|
||||
}
|
||||
|
||||
func testResolvedWindowFrameCentersInFallbackDisplayWhenOffscreen() {
|
||||
let savedFrame = SessionRectSnapshot(x: 4_000, y: 4_000, width: 900, height: 700)
|
||||
let display = AppDelegate.SessionDisplayGeometry(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue