Skip to content

Commit e309184

Browse files
tomerdweissi
authored andcommitted
add flush api to default stdout/stderr logger (#87)
* add flush api to default stdout/stderr logger motivation: default logger may not flush the streams on crush, so need to make flushing moe explicit changes: * add a constructor argument to control flushing behaviour, default to "everytime" * add a "flush" method to allow user defined flush timing. this api is now internal but we could expose it if there is a need * add tests * fixup
1 parent ebdfdcf commit e309184

File tree

3 files changed

+90
-5
lines changed

3 files changed

+90
-5
lines changed

Sources/Logging/Logging.swift

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -531,19 +531,35 @@ public struct MultiplexLogHandler: LogHandler {
531531
/// cross-thread interleaving of output.
532532
internal struct StdioOutputStream: TextOutputStream {
533533
internal let file: UnsafeMutablePointer<FILE>
534+
internal let flushMode: FlushMode
534535

535536
internal func write(_ string: String) {
536537
string.withCString { ptr in
537-
flockfile(file)
538+
flockfile(self.file)
538539
defer {
539-
funlockfile(file)
540+
funlockfile(self.file)
541+
}
542+
_ = fputs(ptr, self.file)
543+
if case .always = self.flushMode {
544+
self.flush()
540545
}
541-
_ = fputs(ptr, file)
542546
}
543547
}
544548

545-
internal static let stderr = StdioOutputStream(file: systemStderr)
546-
internal static let stdout = StdioOutputStream(file: systemStdout)
549+
/// Flush the underlying stream.
550+
/// This has no effect when using the `.always` flush mode, which is the default
551+
internal func flush() {
552+
_ = fflush(self.file)
553+
}
554+
555+
internal static let stderr = StdioOutputStream(file: systemStderr, flushMode: .always)
556+
internal static let stdout = StdioOutputStream(file: systemStdout, flushMode: .always)
557+
558+
/// Defines the flushing strategy for the underlying stream.
559+
internal enum FlushMode {
560+
case undefined
561+
case always
562+
}
547563
}
548564

549565
// Prevent name clashes

Tests/LoggingTests/LoggingTest+XCTest.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ extension LoggingTest {
4242
("testLogLevelCases", testLogLevelCases),
4343
("testLogLevelOrdering", testLogLevelOrdering),
4444
("testStreamLogHandlerWritesToAStream", testStreamLogHandlerWritesToAStream),
45+
("testStdioOutputStreamFlush", testStdioOutputStreamFlush),
4546
]
4647
}
4748
}

Tests/LoggingTests/LoggingTest.swift

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@
1414
@testable import Logging
1515
import XCTest
1616

17+
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
18+
import Darwin
19+
#else
20+
import Glibc
21+
#endif
22+
1723
class LoggingTest: XCTestCase {
1824
func testAutoclosure() throws {
1925
// bootstrap with our test logging impl
@@ -444,4 +450,66 @@ class LoggingTest: XCTestCase {
444450
XCTAssertTrue(messageSucceeded ?? false)
445451
XCTAssertEqual(interceptStream.strings.count, 1)
446452
}
453+
454+
func testStdioOutputStreamFlush() {
455+
// flush on every statement
456+
self.withWriteReadFDsAndReadBuffer { writeFD, readFD, readBuffer in
457+
let logStream = StdioOutputStream(file: writeFD, flushMode: .always)
458+
LoggingSystem.bootstrapInternal { StreamLogHandler(label: $0, stream: logStream) }
459+
Logger(label: "test").critical("test")
460+
461+
let size = read(readFD, readBuffer, 256)
462+
XCTAssertGreaterThan(size, -1, "expected flush")
463+
464+
logStream.flush()
465+
let size2 = read(readFD, readBuffer, 256)
466+
XCTAssertEqual(size2, -1, "expected no flush")
467+
}
468+
// default flushing
469+
self.withWriteReadFDsAndReadBuffer { writeFD, readFD, readBuffer in
470+
let logStream = StdioOutputStream(file: writeFD, flushMode: .undefined)
471+
LoggingSystem.bootstrapInternal { StreamLogHandler(label: $0, stream: logStream) }
472+
Logger(label: "test").critical("test")
473+
474+
let size = read(readFD, readBuffer, 256)
475+
XCTAssertEqual(size, -1, "expected no flush")
476+
477+
logStream.flush()
478+
let size2 = read(readFD, readBuffer, 256)
479+
XCTAssertGreaterThan(size2, -1, "expected flush")
480+
}
481+
}
482+
483+
func withWriteReadFDsAndReadBuffer(_ body: (UnsafeMutablePointer<FILE>, CInt, UnsafeMutablePointer<Int8>) -> Void) {
484+
var fds: [Int32] = [-1, -1]
485+
fds.withUnsafeMutableBufferPointer { ptr in
486+
let err = pipe(ptr.baseAddress!)
487+
XCTAssertEqual(err, 0, "pipe faild \(err)")
488+
}
489+
490+
let writeFD = fdopen(fds[1], "w")
491+
let writeBuffer = UnsafeMutablePointer<Int8>.allocate(capacity: 256)
492+
defer {
493+
writeBuffer.deinitialize(count: 256)
494+
writeBuffer.deallocate()
495+
}
496+
497+
var err = setvbuf(writeFD, writeBuffer, _IOFBF, 256)
498+
XCTAssertEqual(err, 0, "setvbuf faild \(err)")
499+
500+
let readFD = fds[0]
501+
err = fcntl(readFD, F_SETFL, fcntl(readFD, F_GETFL) | O_NONBLOCK)
502+
XCTAssertEqual(err, 0, "fcntl faild \(err)")
503+
504+
let readBuffer = UnsafeMutablePointer<Int8>.allocate(capacity: 256)
505+
defer {
506+
readBuffer.deinitialize(count: 256)
507+
readBuffer.deallocate()
508+
}
509+
510+
// the actual test
511+
body(writeFD!, readFD, readBuffer)
512+
513+
fds.forEach { close($0) }
514+
}
447515
}

0 commit comments

Comments
 (0)