From adf534f7a527e1ad8d22b872379535ea9cacfe0c Mon Sep 17 00:00:00 2001 From: Martin Prusa Date: Thu, 10 Sep 2026 14:35:22 +0200 Subject: [PATCH] FileLogger: use throwing FileHandle API so a failed write cannot crash the app `seekToEndOfFile()` and `write(_:)` raise an Objective-C `NSFileHandleOperationException` when the underlying write fails (e.g. "No space left on device"). Such an exception cannot be caught from Swift, so a full device storage took the whole app down. Switch to `seekToEnd()` / `write(contentsOf:)` / `close()`, which throw a Swift error that lands in the existing `catch` and is reported through `loggerForInternalErrors` instead. Co-Authored-By: Claude Fable 5.1 --- .../Loggers/FileLogger/FileLogger.swift | 8 ++--- .../LoggerTests/Loggers/FileLoggerTests.swift | 35 +++++++++++++++++++ 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/Sources/Logger/Loggers/FileLogger/FileLogger.swift b/Sources/Logger/Loggers/FileLogger/FileLogger.swift index f116ed9..24a5c8a 100644 --- a/Sources/Logger/Loggers/FileLogger/FileLogger.swift +++ b/Sources/Logger/Loggers/FileLogger/FileLogger.swift @@ -68,10 +68,10 @@ public class FileLogger: Logging { } } - private var currentWritableFileHandle: FileHandle? { + var currentWritableFileHandle: FileHandle? { willSet { if currentWritableFileHandle != newValue { - currentWritableFileHandle?.closeFile() + try? currentWritableFileHandle?.close() } } } @@ -264,8 +264,8 @@ public class FileLogger: Logging { throw FileLoggerError.stringToDataConversionFailure } - fileHandle.seekToEndOfFile() - fileHandle.write(data) + try fileHandle.seekToEnd() + try fileHandle.write(contentsOf: data) } catch let error { self.externalLogger("Failed to write to a log file with error: \(error)!") } diff --git a/Tests/LoggerTests/Loggers/FileLoggerTests.swift b/Tests/LoggerTests/Loggers/FileLoggerTests.swift index b5b63f8..7664db2 100644 --- a/Tests/LoggerTests/Loggers/FileLoggerTests.swift +++ b/Tests/LoggerTests/Loggers/FileLoggerTests.swift @@ -196,6 +196,41 @@ class FileLoggerTests: XCTestCase { } } + func test_failed_write_is_reported_instead_of_crashing() throws { + var internalErrors: [String] = [] + + let fileLogger = try FileLogger( + appName: nil, + fileManager: fileManager, + userDefaults: userDefaults, + logDirURL: logDirURL, + namespace: nil, + numberOfLogFiles: 3, + dateFormatter: DateFormatter.dateFormatter, + fileHeaderContent: "", + lineSeparator: "\n", + logEntryEncoder: LogEntryEncoder(), + logEntryDecoder: LogEntryDecoder(), + externalLogger: { internalErrors.append($0) }, + fileAccessQueue: .syncMock + ) + + fileLogger.log(.mock("First message")) + + // Simulate an I/O failure (e.g. "No space left on device") with a handle that cannot be written to. + // The legacy `seekToEndOfFile()` / `write(_:)` API raised an uncatchable NSException here and crashed the app. + fileLogger.currentWritableFileHandle = try FileHandle(forReadingFrom: fileLogger.currentLogFileUrl) + + fileLogger.log(.mock("Second message")) + + XCTAssertEqual(internalErrors.count, 1) + XCTAssertTrue(internalErrors.first?.hasPrefix("Failed to write to a log file") == true, internalErrors.description) + + let content = try String(contentsOf: fileLogger.currentLogFileUrl, encoding: .utf8) + XCTAssertTrue(content.contains("First message"), content) + XCTAssertFalse(content.contains("Second message"), content) + } + func test_file_rotation() throws { let fileLogger = try FileLogger( appName: nil,