1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
|
//
// LogFileOutputStream.swift
// MullvadVPN
//
// Created by pronebird on 02/08/2020.
// Copyright © 2020 Mullvad VPN AB. All rights reserved.
//
import Foundation
import MullvadTypes
/// Interval used for reopening the log file descriptor in the event of failure to open it in
/// the first place, or when writing to it.
private let reopenFileLogInterval: Duration = .seconds(5)
class LogFileOutputStream: TextOutputStream {
private let queue = DispatchQueue(label: "LogFileOutputStreamQueue", qos: .utility)
private let fileURL: URL
private let encoding: String.Encoding
private let maxBufferCapacity: Int
private var state: State = .closed {
didSet {
switch (oldValue, state) {
case (.opened, .waitingToReopen), (.closed, .waitingToReopen):
startTimer()
case (.waitingToReopen, .opened), (.waitingToReopen, .closed):
stopTimer()
default:
break
}
}
}
private var timer: DispatchSourceTimer?
private var buffer = Data()
private enum State {
case closed
case opened(FileHandle)
case waitingToReopen
}
init(fileURL: URL, encoding: String.Encoding = .utf8, maxBufferCapacity: Int = 16 * 1024) {
self.fileURL = fileURL
self.encoding = encoding
self.maxBufferCapacity = maxBufferCapacity
}
deinit {
stopTimer()
}
func write(_ string: String) {
queue.async {
self.writeNoQueue(string)
}
}
private func writeNoQueue(_ string: String) {
guard let data = string.data(using: encoding) else { return }
switch state {
case .closed:
do {
let fileHandle = try openFile()
state = .opened(fileHandle)
try write(fileHandle: fileHandle, data: data)
} catch {
bufferData(data)
state = .waitingToReopen
}
case let .opened(fileHandle):
do {
try write(fileHandle: fileHandle, data: data)
} catch {
bufferData(data)
state = .waitingToReopen
}
case .waitingToReopen:
bufferData(data)
}
}
@discardableResult private func write(fileHandle: FileHandle, data: Data) throws -> Int {
let bytesWritten = data.withUnsafeBytes { buffer -> Int in
guard let ptr = buffer.baseAddress else { return 0 }
return Darwin.write(fileHandle.fileDescriptor, ptr, buffer.count)
}
if bytesWritten == -1 {
let code = POSIXErrorCode(rawValue: errno)!
throw POSIXError(code)
} else {
return bytesWritten
}
}
private func openFile() throws -> FileHandle {
let oflag: Int32 = O_WRONLY | O_CREAT
let mode: mode_t = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH
let fd = fileURL.path.withCString { Darwin.open($0, oflag, mode) }
if fd == -1 {
let code = POSIXErrorCode(rawValue: errno)!
throw POSIXError(code)
} else {
return FileHandle(fileDescriptor: fd, closeOnDealloc: true)
}
}
private func startTimer() {
timer?.cancel()
let timer = DispatchSource.makeTimerSource(queue: queue)
timer.setEventHandler { [weak self] in
self?.reopenFile()
}
timer.schedule(
wallDeadline: .now() + reopenFileLogInterval,
repeating: reopenFileLogInterval.timeInterval
)
timer.activate()
self.timer = timer
}
private func stopTimer() {
timer?.cancel()
timer = nil
}
private func reopenFile() {
do {
let fileHandle = try openFile()
// Write a message indicating that the file was reopened.
let messageData =
"<Log file re-opened after failure. Buffered \(buffer.count) bytes of messages>\n"
.data(using: encoding, allowLossyConversion: true)!
try write(fileHandle: fileHandle, data: messageData)
// Write all buffered messages.
if !buffer.isEmpty {
try write(fileHandle: fileHandle, data: buffer)
buffer.removeAll()
}
state = .opened(fileHandle)
} catch {
state = .waitingToReopen
}
}
private func bufferData(_ data: Data) {
buffer.append(data)
if buffer.count > maxBufferCapacity {
buffer.removeFirst(buffer.count - maxBufferCapacity)
}
}
}
|