react-native-nitro-logger 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/NitroLogger.podspec +29 -0
- package/README.md +214 -0
- package/android/CMakeLists.txt +24 -0
- package/android/build.gradle +124 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/cpp-adapter.cpp +11 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/AndroidPlatformIo.kt +100 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/HybridFileSink.kt +292 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/HybridNativeConsoleSink.kt +23 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/LogFileWriter.kt +1328 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/LogRotationPolicy.kt +186 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/LogSecureFile.kt +80 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/LogWriterRegistry.kt +420 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/NativeConsoleWriter.kt +237 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/NitroLoggerPackage.kt +22 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/PlatformIo.kt +165 -0
- package/docs/PARITY.md +330 -0
- package/docs/PRIVACY.md +341 -0
- package/eslint-plugin/README.md +238 -0
- package/eslint-plugin/index.js +62 -0
- package/eslint-plugin/rules/literal-subsystem.js +102 -0
- package/eslint-plugin/rules/no-computed-metadata-key.js +174 -0
- package/eslint-plugin/rules/no-derived-correlation.js +171 -0
- package/eslint-plugin/rules/no-dynamic-message.js +60 -0
- package/eslint-plugin/shared.js +1948 -0
- package/ios/Gzip.swift +203 -0
- package/ios/HybridFileSink.swift +291 -0
- package/ios/HybridNativeConsoleSink.swift +20 -0
- package/ios/LogFileWriter.swift +1280 -0
- package/ios/LogSecureFile.swift +296 -0
- package/ios/LogWriterRegistry.swift +444 -0
- package/ios/NativeConsoleWriter.swift +180 -0
- package/lib/module/Logger.js +381 -0
- package/lib/module/Logger.js.map +1 -0
- package/lib/module/ScopedLogger.js +70 -0
- package/lib/module/ScopedLogger.js.map +1 -0
- package/lib/module/config.js +23 -0
- package/lib/module/config.js.map +1 -0
- package/lib/module/destinations/Batcher.js +647 -0
- package/lib/module/destinations/Batcher.js.map +1 -0
- package/lib/module/destinations/ConsoleDestination.js +51 -0
- package/lib/module/destinations/ConsoleDestination.js.map +1 -0
- package/lib/module/destinations/FileDestination.js +360 -0
- package/lib/module/destinations/FileDestination.js.map +1 -0
- package/lib/module/destinations/NativeConsoleDestination.js +170 -0
- package/lib/module/destinations/NativeConsoleDestination.js.map +1 -0
- package/lib/module/destinations/types.js +4 -0
- package/lib/module/destinations/types.js.map +1 -0
- package/lib/module/formatters/DefaultFormatter.js +136 -0
- package/lib/module/formatters/DefaultFormatter.js.map +1 -0
- package/lib/module/formatters/JsonLinesFormatter.js +162 -0
- package/lib/module/formatters/JsonLinesFormatter.js.map +1 -0
- package/lib/module/formatters/timestamp.js +15 -0
- package/lib/module/formatters/timestamp.js.map +1 -0
- package/lib/module/formatters/types.js +4 -0
- package/lib/module/formatters/types.js.map +1 -0
- package/lib/module/index.js +31 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/integrations/appState.js +81 -0
- package/lib/module/integrations/appState.js.map +1 -0
- package/lib/module/integrations/errorHandler.js +166 -0
- package/lib/module/integrations/errorHandler.js.map +1 -0
- package/lib/module/integrations/sanitizeError.js +238 -0
- package/lib/module/integrations/sanitizeError.js.map +1 -0
- package/lib/module/levels.js +35 -0
- package/lib/module/levels.js.map +1 -0
- package/lib/module/metadata.js +52 -0
- package/lib/module/metadata.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/privacy.js +349 -0
- package/lib/module/privacy.js.map +1 -0
- package/lib/module/specs/FileSink.nitro.js +4 -0
- package/lib/module/specs/FileSink.nitro.js.map +1 -0
- package/lib/module/specs/NativeConsoleSink.nitro.js +4 -0
- package/lib/module/specs/NativeConsoleSink.nitro.js.map +1 -0
- package/lib/module/types.js +4 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/utf8.js +42 -0
- package/lib/module/utf8.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/Logger.d.ts +122 -0
- package/lib/typescript/src/Logger.d.ts.map +1 -0
- package/lib/typescript/src/ScopedLogger.d.ts +36 -0
- package/lib/typescript/src/ScopedLogger.d.ts.map +1 -0
- package/lib/typescript/src/config.d.ts +12 -0
- package/lib/typescript/src/config.d.ts.map +1 -0
- package/lib/typescript/src/destinations/Batcher.d.ts +244 -0
- package/lib/typescript/src/destinations/Batcher.d.ts.map +1 -0
- package/lib/typescript/src/destinations/ConsoleDestination.d.ts +27 -0
- package/lib/typescript/src/destinations/ConsoleDestination.d.ts.map +1 -0
- package/lib/typescript/src/destinations/FileDestination.d.ts +200 -0
- package/lib/typescript/src/destinations/FileDestination.d.ts.map +1 -0
- package/lib/typescript/src/destinations/NativeConsoleDestination.d.ts +86 -0
- package/lib/typescript/src/destinations/NativeConsoleDestination.d.ts.map +1 -0
- package/lib/typescript/src/destinations/types.d.ts +19 -0
- package/lib/typescript/src/destinations/types.d.ts.map +1 -0
- package/lib/typescript/src/formatters/DefaultFormatter.d.ts +22 -0
- package/lib/typescript/src/formatters/DefaultFormatter.d.ts.map +1 -0
- package/lib/typescript/src/formatters/JsonLinesFormatter.d.ts +73 -0
- package/lib/typescript/src/formatters/JsonLinesFormatter.d.ts.map +1 -0
- package/lib/typescript/src/formatters/timestamp.d.ts +6 -0
- package/lib/typescript/src/formatters/timestamp.d.ts.map +1 -0
- package/lib/typescript/src/formatters/types.d.ts +27 -0
- package/lib/typescript/src/formatters/types.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +34 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/integrations/appState.d.ts +33 -0
- package/lib/typescript/src/integrations/appState.d.ts.map +1 -0
- package/lib/typescript/src/integrations/errorHandler.d.ts +55 -0
- package/lib/typescript/src/integrations/errorHandler.d.ts.map +1 -0
- package/lib/typescript/src/integrations/sanitizeError.d.ts +89 -0
- package/lib/typescript/src/integrations/sanitizeError.d.ts.map +1 -0
- package/lib/typescript/src/levels.d.ts +9 -0
- package/lib/typescript/src/levels.d.ts.map +1 -0
- package/lib/typescript/src/metadata.d.ts +23 -0
- package/lib/typescript/src/metadata.d.ts.map +1 -0
- package/lib/typescript/src/privacy.d.ts +103 -0
- package/lib/typescript/src/privacy.d.ts.map +1 -0
- package/lib/typescript/src/specs/FileSink.nitro.d.ts +131 -0
- package/lib/typescript/src/specs/FileSink.nitro.d.ts.map +1 -0
- package/lib/typescript/src/specs/NativeConsoleSink.nitro.d.ts +21 -0
- package/lib/typescript/src/specs/NativeConsoleSink.nitro.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +37 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/lib/typescript/src/utf8.d.ts +15 -0
- package/lib/typescript/src/utf8.d.ts.map +1 -0
- package/nitro.json +33 -0
- package/nitrogen/generated/android/c++/JAppendResult.hpp +79 -0
- package/nitrogen/generated/android/c++/JClearOutcome.hpp +88 -0
- package/nitrogen/generated/android/c++/JFlushOutcome.hpp +81 -0
- package/nitrogen/generated/android/c++/JHybridFileSinkSpec.cpp +120 -0
- package/nitrogen/generated/android/c++/JHybridFileSinkSpec.hpp +69 -0
- package/nitrogen/generated/android/c++/JHybridNativeConsoleSinkSpec.cpp +71 -0
- package/nitrogen/generated/android/c++/JHybridNativeConsoleSinkSpec.hpp +64 -0
- package/nitrogen/generated/android/c++/JRejectReason.hpp +64 -0
- package/nitrogen/generated/android/c++/JRotationConfig.hpp +77 -0
- package/nitrogen/generated/android/c++/JSinkStatus.hpp +69 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/AppendResult.kt +76 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/ClearOutcome.kt +66 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/FlushOutcome.kt +81 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/HybridFileSinkSpec.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/HybridNativeConsoleSinkSpec.kt +58 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/RejectReason.kt +25 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/RotationConfig.kt +76 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/SinkStatus.kt +66 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/nitrologgerOnLoad.kt +35 -0
- package/nitrogen/generated/android/nitrologger+autolinking.cmake +83 -0
- package/nitrogen/generated/android/nitrologger+autolinking.gradle +27 -0
- package/nitrogen/generated/android/nitrologgerOnLoad.cpp +70 -0
- package/nitrogen/generated/android/nitrologgerOnLoad.hpp +34 -0
- package/nitrogen/generated/ios/NitroLogger+autolinking.rb +62 -0
- package/nitrogen/generated/ios/NitroLogger-Swift-Cxx-Bridge.cpp +50 -0
- package/nitrogen/generated/ios/NitroLogger-Swift-Cxx-Bridge.hpp +216 -0
- package/nitrogen/generated/ios/NitroLogger-Swift-Cxx-Umbrella.hpp +69 -0
- package/nitrogen/generated/ios/NitroLoggerAutolinking.mm +41 -0
- package/nitrogen/generated/ios/NitroLoggerAutolinking.swift +38 -0
- package/nitrogen/generated/ios/c++/HybridFileSinkSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridFileSinkSpecSwift.hpp +150 -0
- package/nitrogen/generated/ios/c++/HybridNativeConsoleSinkSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridNativeConsoleSinkSpecSwift.hpp +87 -0
- package/nitrogen/generated/ios/swift/AppendResult.swift +60 -0
- package/nitrogen/generated/ios/swift/ClearOutcome.swift +50 -0
- package/nitrogen/generated/ios/swift/FlushOutcome.swift +59 -0
- package/nitrogen/generated/ios/swift/HybridFileSinkSpec.swift +61 -0
- package/nitrogen/generated/ios/swift/HybridFileSinkSpec_cxx.swift +227 -0
- package/nitrogen/generated/ios/swift/HybridNativeConsoleSinkSpec.swift +56 -0
- package/nitrogen/generated/ios/swift/HybridNativeConsoleSinkSpec_cxx.swift +148 -0
- package/nitrogen/generated/ios/swift/RejectReason.swift +48 -0
- package/nitrogen/generated/ios/swift/RotationConfig.swift +93 -0
- package/nitrogen/generated/ios/swift/SinkStatus.swift +44 -0
- package/nitrogen/generated/shared/c++/AppendResult.hpp +105 -0
- package/nitrogen/generated/shared/c++/ClearOutcome.hpp +96 -0
- package/nitrogen/generated/shared/c++/FlushOutcome.hpp +107 -0
- package/nitrogen/generated/shared/c++/HybridFileSinkSpec.cpp +28 -0
- package/nitrogen/generated/shared/c++/HybridFileSinkSpec.hpp +84 -0
- package/nitrogen/generated/shared/c++/HybridNativeConsoleSinkSpec.cpp +22 -0
- package/nitrogen/generated/shared/c++/HybridNativeConsoleSinkSpec.hpp +64 -0
- package/nitrogen/generated/shared/c++/RejectReason.hpp +84 -0
- package/nitrogen/generated/shared/c++/RotationConfig.hpp +103 -0
- package/nitrogen/generated/shared/c++/SinkStatus.hpp +95 -0
- package/package.json +151 -0
- package/src/Logger.ts +417 -0
- package/src/ScopedLogger.ts +94 -0
- package/src/config.ts +25 -0
- package/src/destinations/Batcher.ts +805 -0
- package/src/destinations/ConsoleDestination.ts +60 -0
- package/src/destinations/FileDestination.ts +473 -0
- package/src/destinations/NativeConsoleDestination.ts +220 -0
- package/src/destinations/types.ts +19 -0
- package/src/formatters/DefaultFormatter.ts +150 -0
- package/src/formatters/JsonLinesFormatter.ts +195 -0
- package/src/formatters/timestamp.ts +12 -0
- package/src/formatters/types.ts +29 -0
- package/src/index.tsx +106 -0
- package/src/integrations/appState.ts +111 -0
- package/src/integrations/errorHandler.ts +223 -0
- package/src/integrations/sanitizeError.ts +287 -0
- package/src/levels.ts +35 -0
- package/src/metadata.ts +52 -0
- package/src/privacy.ts +389 -0
- package/src/specs/FileSink.nitro.ts +144 -0
- package/src/specs/NativeConsoleSink.nitro.ts +21 -0
- package/src/types.ts +45 -0
- package/src/utf8.ts +39 -0
package/ios/Gzip.swift
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
#if canImport(Compression)
|
|
2
|
+
import Foundation
|
|
3
|
+
import Compression
|
|
4
|
+
|
|
5
|
+
/// Produces gzip (RFC 1952) containers for rotated log archives.
|
|
6
|
+
///
|
|
7
|
+
/// Apple's Compression framework emits a *raw* DEFLATE stream for
|
|
8
|
+
/// `COMPRESSION_ZLIB` (RFC 1951) with no container around it, so writing a file
|
|
9
|
+
/// that `gunzip` and every other standard tool can open means supplying the
|
|
10
|
+
/// gzip header and the trailing CRC-32 and length ourselves.
|
|
11
|
+
///
|
|
12
|
+
/// Vendored unchanged from SwiftLogger (`Sources/Logger/Gzip.swift`). Kept
|
|
13
|
+
/// byte-identical on purpose: it is the piece with the least reason to diverge
|
|
14
|
+
/// and the most reason to stay diffable against the original.
|
|
15
|
+
internal enum Gzip {
|
|
16
|
+
|
|
17
|
+
/// Header: magic, DEFLATE method, no flags, no mtime, no extra flags, and
|
|
18
|
+
/// an unknown OS (255) so the output is reproducible.
|
|
19
|
+
private static let header: [UInt8] = [0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff]
|
|
20
|
+
|
|
21
|
+
/// Returns `data` as a gzip stream, or `nil` if compression fails.
|
|
22
|
+
static func compress(_ data: Data) -> Data? {
|
|
23
|
+
guard let deflated = deflate(data) else { return nil }
|
|
24
|
+
|
|
25
|
+
var output = Data(capacity: deflated.count + 18)
|
|
26
|
+
output.append(contentsOf: header)
|
|
27
|
+
output.append(deflated)
|
|
28
|
+
|
|
29
|
+
// Trailer: CRC-32 of the *uncompressed* data, then its length mod 2^32,
|
|
30
|
+
// both little-endian.
|
|
31
|
+
appendLittleEndian(&output, crc32(data))
|
|
32
|
+
appendLittleEndian(&output, UInt32(truncatingIfNeeded: data.count))
|
|
33
|
+
|
|
34
|
+
return output
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/// Size of each read from the source file. Two of these are live at a time,
|
|
38
|
+
/// so peak memory is bounded regardless of how large the archive is.
|
|
39
|
+
private static let chunkSize = 64 * 1024
|
|
40
|
+
|
|
41
|
+
/// Compresses the file at `source` into a gzip file at `destination`,
|
|
42
|
+
/// streaming in fixed-size chunks.
|
|
43
|
+
///
|
|
44
|
+
/// ``compress(_:)`` holds the whole input, the whole DEFLATE output, and
|
|
45
|
+
/// the assembled container in memory at once — roughly 2.5× a 10 MB archive
|
|
46
|
+
/// on the write queue. This reads and deflates incrementally instead, so a
|
|
47
|
+
/// rotation costs two 64 KB buffers no matter how big the log grew.
|
|
48
|
+
///
|
|
49
|
+
/// Returns `false` and removes any partial output if anything fails, so the
|
|
50
|
+
/// caller can keep the uncompressed original.
|
|
51
|
+
static func compressFile(at source: URL, to destination: URL) -> Bool {
|
|
52
|
+
guard let input = try? FileHandle(forReadingFrom: source) else { return false }
|
|
53
|
+
defer { try? input.close() }
|
|
54
|
+
|
|
55
|
+
guard FileManager.default.createFile(atPath: destination.path, contents: nil),
|
|
56
|
+
let output = try? FileHandle(forWritingTo: destination) else {
|
|
57
|
+
try? FileManager.default.removeItem(at: destination)
|
|
58
|
+
return false
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
var succeeded = false
|
|
62
|
+
defer {
|
|
63
|
+
try? output.close()
|
|
64
|
+
if !succeeded {
|
|
65
|
+
try? FileManager.default.removeItem(at: destination)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
guard (try? output.write(contentsOf: Data(header))) != nil else { return false }
|
|
70
|
+
|
|
71
|
+
var stream = compression_stream(
|
|
72
|
+
dst_ptr: UnsafeMutablePointer<UInt8>(bitPattern: -1)!, dst_size: 0,
|
|
73
|
+
src_ptr: UnsafePointer<UInt8>(bitPattern: -1)!, src_size: 0,
|
|
74
|
+
state: nil
|
|
75
|
+
)
|
|
76
|
+
guard compression_stream_init(&stream, COMPRESSION_STREAM_ENCODE, COMPRESSION_ZLIB)
|
|
77
|
+
== COMPRESSION_STATUS_OK else { return false }
|
|
78
|
+
defer { compression_stream_destroy(&stream) }
|
|
79
|
+
|
|
80
|
+
let outputBuffer = UnsafeMutablePointer<UInt8>.allocate(capacity: chunkSize)
|
|
81
|
+
defer { outputBuffer.deallocate() }
|
|
82
|
+
|
|
83
|
+
var crc: UInt32 = 0xFFFF_FFFF
|
|
84
|
+
var totalRead = 0
|
|
85
|
+
|
|
86
|
+
while true {
|
|
87
|
+
// A read error must NOT be treated as end-of-input. Doing so would
|
|
88
|
+
// finalize the stream over a partial file, write a CRC and length
|
|
89
|
+
// matching what was read, and return success — producing a .gz that
|
|
90
|
+
// gunzip happily verifies while the caller deletes the complete
|
|
91
|
+
// original. Silent truncation is far worse than a failed rotation.
|
|
92
|
+
//
|
|
93
|
+
// `read(upToCount:)` returns nil at EOF and throws on error, so the
|
|
94
|
+
// two must be distinguished rather than collapsed with `try?`.
|
|
95
|
+
let chunk: Data
|
|
96
|
+
do {
|
|
97
|
+
chunk = try input.read(upToCount: chunkSize) ?? Data()
|
|
98
|
+
} catch {
|
|
99
|
+
return false
|
|
100
|
+
}
|
|
101
|
+
let isLast = chunk.isEmpty
|
|
102
|
+
totalRead += chunk.count
|
|
103
|
+
crc = crc32Update(crc, chunk)
|
|
104
|
+
|
|
105
|
+
let ok = chunk.withUnsafeBytes { raw -> Bool in
|
|
106
|
+
// An empty Data has no base address; the encoder accepts a
|
|
107
|
+
// zero-length source as long as the pointer is non-null.
|
|
108
|
+
let base = raw.bindMemory(to: UInt8.self).baseAddress
|
|
109
|
+
?? UnsafePointer<UInt8>(bitPattern: -1)!
|
|
110
|
+
stream.src_ptr = base
|
|
111
|
+
stream.src_size = chunk.count
|
|
112
|
+
let flags = isLast ? Int32(COMPRESSION_STREAM_FINALIZE.rawValue) : 0
|
|
113
|
+
|
|
114
|
+
// One source chunk can expand into several output chunks, so
|
|
115
|
+
// keep draining until the encoder stops filling the buffer.
|
|
116
|
+
repeat {
|
|
117
|
+
stream.dst_ptr = outputBuffer
|
|
118
|
+
stream.dst_size = chunkSize
|
|
119
|
+
let status = compression_stream_process(&stream, flags)
|
|
120
|
+
guard status != COMPRESSION_STATUS_ERROR else { return false }
|
|
121
|
+
|
|
122
|
+
let produced = chunkSize - stream.dst_size
|
|
123
|
+
if produced > 0 {
|
|
124
|
+
let out = Data(bytes: outputBuffer, count: produced)
|
|
125
|
+
guard (try? output.write(contentsOf: out)) != nil else { return false }
|
|
126
|
+
}
|
|
127
|
+
if status == COMPRESSION_STATUS_END { break }
|
|
128
|
+
} while stream.src_size > 0 || (isLast && stream.dst_size == 0)
|
|
129
|
+
|
|
130
|
+
return true
|
|
131
|
+
}
|
|
132
|
+
guard ok else { return false }
|
|
133
|
+
if isLast { break }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Trailer: CRC-32 of the uncompressed bytes, then their length mod 2^32.
|
|
137
|
+
var trailer = Data(capacity: 8)
|
|
138
|
+
appendLittleEndian(&trailer, crc ^ 0xFFFF_FFFF)
|
|
139
|
+
appendLittleEndian(&trailer, UInt32(truncatingIfNeeded: totalRead))
|
|
140
|
+
guard (try? output.write(contentsOf: trailer)) != nil else { return false }
|
|
141
|
+
|
|
142
|
+
succeeded = true
|
|
143
|
+
return true
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private static func appendLittleEndian(_ data: inout Data, _ value: UInt32) {
|
|
147
|
+
withUnsafeBytes(of: value.littleEndian) { data.append(contentsOf: $0) }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private static func deflate(_ data: Data) -> Data? {
|
|
151
|
+
guard !data.isEmpty else { return Data() }
|
|
152
|
+
|
|
153
|
+
// Incompressible input can grow slightly, so leave headroom rather than
|
|
154
|
+
// letting the encode fail on a tight buffer.
|
|
155
|
+
let capacity = data.count + (data.count / 2) + 64
|
|
156
|
+
let destination = UnsafeMutablePointer<UInt8>.allocate(capacity: capacity)
|
|
157
|
+
defer { destination.deallocate() }
|
|
158
|
+
|
|
159
|
+
let written = data.withUnsafeBytes { raw -> Int in
|
|
160
|
+
guard let base = raw.bindMemory(to: UInt8.self).baseAddress else { return 0 }
|
|
161
|
+
return compression_encode_buffer(
|
|
162
|
+
destination, capacity,
|
|
163
|
+
base, data.count,
|
|
164
|
+
nil,
|
|
165
|
+
COMPRESSION_ZLIB
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
guard written > 0 else { return nil }
|
|
170
|
+
return Data(bytes: destination, count: written)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// MARK: - CRC-32
|
|
174
|
+
|
|
175
|
+
private static let crcTable: [UInt32] = {
|
|
176
|
+
(0..<256).map { index -> UInt32 in
|
|
177
|
+
var value = UInt32(index)
|
|
178
|
+
for _ in 0..<8 {
|
|
179
|
+
value = (value & 1 == 1) ? (0xEDB8_8320 ^ (value >> 1)) : (value >> 1)
|
|
180
|
+
}
|
|
181
|
+
return value
|
|
182
|
+
}
|
|
183
|
+
}()
|
|
184
|
+
|
|
185
|
+
static func crc32(_ data: Data) -> UInt32 {
|
|
186
|
+
crc32Update(0xFFFF_FFFF, data) ^ 0xFFFF_FFFF
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/// Folds `data` into a running CRC-32. The caller supplies the initial
|
|
190
|
+
/// `0xFFFF_FFFF` and applies the final inversion, so a checksum can be
|
|
191
|
+
/// accumulated across streamed chunks.
|
|
192
|
+
private static func crc32Update(_ crc: UInt32, _ data: Data) -> UInt32 {
|
|
193
|
+
guard !data.isEmpty else { return crc }
|
|
194
|
+
var value = crc
|
|
195
|
+
data.withUnsafeBytes { raw in
|
|
196
|
+
for byte in raw.bindMemory(to: UInt8.self) {
|
|
197
|
+
value = crcTable[Int((value ^ UInt32(byte)) & 0xFF)] ^ (value >> 8)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return value
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
#endif
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import NitroModules
|
|
3
|
+
|
|
4
|
+
/// The Nitro end of the file sink: marshalling, and nothing else.
|
|
5
|
+
///
|
|
6
|
+
/// Every decision — batching, backpressure, drop accounting, loss notices —
|
|
7
|
+
/// lives in TypeScript, and every byte that touches the disk lives in
|
|
8
|
+
/// `LogWriter`. What is left here is converting `Double` to `Int`, one enum to
|
|
9
|
+
/// another, and holding exactly one `LogFileHandle`.
|
|
10
|
+
///
|
|
11
|
+
/// That split is the point, not an accident of it: `LogWriter` imports nothing
|
|
12
|
+
/// from Nitro, so rotation, recovery, the registry, and every injected fault run
|
|
13
|
+
/// under XCTest in about a second instead of by hand on a simulator. Anything
|
|
14
|
+
/// that grows logic in this file has been put in the wrong place.
|
|
15
|
+
final class HybridFileSink: HybridFileSinkSpec {
|
|
16
|
+
private let lock = NSLock()
|
|
17
|
+
private var handle: LogFileHandle?
|
|
18
|
+
|
|
19
|
+
/// Whether this sink may have put anything on disk.
|
|
20
|
+
///
|
|
21
|
+
/// `handle == nil` conflates two states that owe the caller opposite answers.
|
|
22
|
+
/// A sink that created nothing can say "everything is deleted" vacuously. A
|
|
23
|
+
/// sink whose files are on disk but out of reach cannot — and reporting a
|
|
24
|
+
/// durable purge over those is the worst lie in the API, because the only
|
|
25
|
+
/// caller who asks is the one deleting patient data on request.
|
|
26
|
+
///
|
|
27
|
+
/// Set *before* the acquisition attempt rather than after it succeeds.
|
|
28
|
+
/// Acquiring creates the log directory and can then fail on the file itself,
|
|
29
|
+
/// so a throw out of `acquire` is not evidence that nothing was written.
|
|
30
|
+
///
|
|
31
|
+
/// Never cleared once set: `close` releases the handle, it does not unmake
|
|
32
|
+
/// the files.
|
|
33
|
+
private var mayHaveArtifacts = false
|
|
34
|
+
|
|
35
|
+
/// An `open` past its check and inside `acquire`.
|
|
36
|
+
///
|
|
37
|
+
/// Published so a second `open` is refused instead of racing to install a
|
|
38
|
+
/// handle over the first one's, and so `close` can tell "nothing here" from
|
|
39
|
+
/// "something is on its way".
|
|
40
|
+
private var opening = false
|
|
41
|
+
|
|
42
|
+
/// A `close` that arrived while `opening` was true.
|
|
43
|
+
///
|
|
44
|
+
/// The close had no handle to flush and has already returned; this tells the
|
|
45
|
+
/// in-flight `open` to throw away what it acquired rather than install it.
|
|
46
|
+
private var closePending = false
|
|
47
|
+
|
|
48
|
+
/// The native finalizer, which is the whole reason the refcount is native.
|
|
49
|
+
///
|
|
50
|
+
/// An abrupt runtime teardown never runs JavaScript, so a JS `dispose()` is
|
|
51
|
+
/// not a guarantee — but this deinit is. Releasing here hands the descriptor
|
|
52
|
+
/// and the registry slot back even when nothing on the JS side got to run.
|
|
53
|
+
deinit {
|
|
54
|
+
lock.lock()
|
|
55
|
+
let live = handle
|
|
56
|
+
handle = nil
|
|
57
|
+
lock.unlock()
|
|
58
|
+
// Zero deadline: a teardown deinit must not wait on a wedged disk.
|
|
59
|
+
_ = live?.close(deadlineMs: 0)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
var defaultLogDirectory: String {
|
|
63
|
+
let base = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first
|
|
64
|
+
?? FileManager.default.temporaryDirectory
|
|
65
|
+
return base.appendingPathComponent("Logs").path
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
func open(path: String, rotation: RotationConfig?, lineFramed: Bool?) throws {
|
|
69
|
+
lock.lock()
|
|
70
|
+
// `opening` counts as open. Acquisition does real I/O — it creates the
|
|
71
|
+
// directory, opens the file, and may scan a torn tail — so the lock cannot
|
|
72
|
+
// be held across it without putting every `getStatus` behind disk latency,
|
|
73
|
+
// which is the one thing that call is documented not to do. Instead the
|
|
74
|
+
// in-flight attempt is published, and a second one is refused rather than
|
|
75
|
+
// being allowed to acquire a writer that would overwrite the first: the
|
|
76
|
+
// loser's handle would be unreachable, and unreachable means a later purge
|
|
77
|
+
// never deletes its files.
|
|
78
|
+
let busy = handle != nil || opening
|
|
79
|
+
if !busy {
|
|
80
|
+
opening = true
|
|
81
|
+
closePending = false
|
|
82
|
+
// Recorded before the attempt: see `mayHaveArtifacts`.
|
|
83
|
+
mayHaveArtifacts = true
|
|
84
|
+
}
|
|
85
|
+
lock.unlock()
|
|
86
|
+
guard !busy else {
|
|
87
|
+
throw RuntimeError.error(withMessage: "FileSink: already open")
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// However this attempt ends, it is no longer in flight afterwards.
|
|
91
|
+
defer {
|
|
92
|
+
lock.lock()
|
|
93
|
+
opening = false
|
|
94
|
+
lock.unlock()
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let acquired: LogFileHandle
|
|
98
|
+
do {
|
|
99
|
+
acquired = try LogWriterRegistry.shared.acquire(
|
|
100
|
+
path: path,
|
|
101
|
+
policy: Self.policy(from: rotation),
|
|
102
|
+
// Absent means absent. Without a declared one-record-per-line contract
|
|
103
|
+
// the startup scan must not trim a trailing record, because it cannot
|
|
104
|
+
// tell a torn one from an intentional newline.
|
|
105
|
+
lineFramed: lineFramed ?? false
|
|
106
|
+
)
|
|
107
|
+
} catch LogWriterError.configConflict {
|
|
108
|
+
throw RuntimeError.error(
|
|
109
|
+
withMessage: "FileSink: another destination already opened this file with a different configuration")
|
|
110
|
+
} catch LogWriterError.symlinkEscape {
|
|
111
|
+
throw RuntimeError.error(withMessage: "FileSink: the log path is a symbolic link")
|
|
112
|
+
} catch LogWriterError.stillClosing {
|
|
113
|
+
// Distinct from the others because it is the one worth retrying: a
|
|
114
|
+
// previous destination on this file has not finished shutting down, and
|
|
115
|
+
// opening a second writer alongside it is exactly what must not happen.
|
|
116
|
+
throw RuntimeError.error(
|
|
117
|
+
withMessage: "FileSink: the previous destination for this file is still closing")
|
|
118
|
+
} catch {
|
|
119
|
+
// Deliberately payload-free. An `errno` description or a path is exactly
|
|
120
|
+
// the kind of string that carries a username, and this message crosses
|
|
121
|
+
// into JavaScript where something will eventually log it.
|
|
122
|
+
throw RuntimeError.error(withMessage: "FileSink: could not open the log file")
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
lock.lock()
|
|
126
|
+
// A close that arrived mid-acquisition found nothing to hand back and has
|
|
127
|
+
// already returned. Installing now would leave a live writer holding a
|
|
128
|
+
// descriptor that nothing can reach or release.
|
|
129
|
+
let abandon = closePending
|
|
130
|
+
closePending = false
|
|
131
|
+
if !abandon { handle = acquired }
|
|
132
|
+
lock.unlock()
|
|
133
|
+
|
|
134
|
+
if abandon {
|
|
135
|
+
// Zero deadline: the caller has already been told this sink is closed.
|
|
136
|
+
_ = acquired.close(deadlineMs: 0)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
func appendBatch(batch: String, entryCount: Double) throws -> AppendResult {
|
|
141
|
+
guard let handle = current() else {
|
|
142
|
+
return AppendResult(accepted: false, rejectReason: .closed,
|
|
143
|
+
queuedBytes: 0, lostBytes: 0, lostEntries: 0, degraded: 0)
|
|
144
|
+
}
|
|
145
|
+
// Refused rather than rounded: an unchecked cast of NaN or 1e30 traps, and
|
|
146
|
+
// a count that does not survive the round trip cannot be trusted to
|
|
147
|
+
// describe the batch it arrived with.
|
|
148
|
+
guard entryCount.isFinite, let count = Int(exactly: entryCount) else {
|
|
149
|
+
return Self.appendResult(
|
|
150
|
+
LogAppendResult(accepted: false, rejectReason: .failed, status: handle.status()))
|
|
151
|
+
}
|
|
152
|
+
return Self.appendResult(handle.appendBatch(batch, entryCount: count))
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
func getStatus() throws -> SinkStatus {
|
|
156
|
+
guard let handle = current() else {
|
|
157
|
+
return SinkStatus(queuedBytes: 0, lostBytes: 0, lostEntries: 0, degraded: 0)
|
|
158
|
+
}
|
|
159
|
+
return Self.status(handle.status())
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
func flush(deadlineMs: Double) throws -> FlushOutcome {
|
|
163
|
+
guard let handle = current() else {
|
|
164
|
+
// Nothing was ever accepted through this sink, so "every accepted byte
|
|
165
|
+
// reached storage" holds vacuously. Reporting otherwise would have the
|
|
166
|
+
// JavaScript side retry a flush that can never have anything to do.
|
|
167
|
+
return Self.emptyOutcome()
|
|
168
|
+
}
|
|
169
|
+
return Self.flushOutcome(handle.flush(deadlineMs: deadlineMs))
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
func close(deadlineMs: Double) throws -> FlushOutcome {
|
|
173
|
+
lock.lock()
|
|
174
|
+
let live = handle
|
|
175
|
+
handle = nil
|
|
176
|
+
// Nothing to close, but an acquisition is in flight and will finish after
|
|
177
|
+
// this returns. Recording the intent is what keeps that writer from being
|
|
178
|
+
// installed into a sink the caller has already closed.
|
|
179
|
+
if live == nil && opening { closePending = true }
|
|
180
|
+
lock.unlock()
|
|
181
|
+
|
|
182
|
+
guard let live else { return Self.emptyOutcome() }
|
|
183
|
+
return Self.flushOutcome(live.close(deadlineMs: deadlineMs))
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
func getLogFilePaths() throws -> [String] {
|
|
187
|
+
current()?.logFilePaths() ?? []
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
func clearLogs(deadlineMs: Double) throws -> ClearOutcome {
|
|
191
|
+
// Both fields read in ONE critical section. Reading them separately lets a
|
|
192
|
+
// close land in between and produce "no handle, nothing created" — the one
|
|
193
|
+
// combination that is never true, and the one that lies in the direction
|
|
194
|
+
// that matters.
|
|
195
|
+
lock.lock()
|
|
196
|
+
let live = handle
|
|
197
|
+
let created = mayHaveArtifacts
|
|
198
|
+
lock.unlock()
|
|
199
|
+
|
|
200
|
+
guard let handle = live else {
|
|
201
|
+
// Nothing created: "every artifact is gone" holds vacuously. `rebound`
|
|
202
|
+
// stays false regardless — there is nothing to rebind onto, and saying
|
|
203
|
+
// otherwise would have the JS destination resume against a sink it never
|
|
204
|
+
// opened.
|
|
205
|
+
//
|
|
206
|
+
// Otherwise the files are still there and this object cannot reach them.
|
|
207
|
+
// The registry has the right answer for a released handle
|
|
208
|
+
// (`durable: false`, pinned by `testAReleasedHandleCannotPurge`) but
|
|
209
|
+
// `close` nils the handle above before that branch is reachable, so the
|
|
210
|
+
// honest answer has to be produced here.
|
|
211
|
+
return ClearOutcome(
|
|
212
|
+
deletedCount: 0, failedPaths: [], durable: !created, rebound: false)
|
|
213
|
+
}
|
|
214
|
+
let outcome = handle.clearLogs(deadlineMs: deadlineMs)
|
|
215
|
+
return ClearOutcome(
|
|
216
|
+
deletedCount: Double(outcome.deletedCount),
|
|
217
|
+
failedPaths: outcome.failedPaths,
|
|
218
|
+
durable: outcome.durable,
|
|
219
|
+
rebound: outcome.rebound
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private func current() -> LogFileHandle? {
|
|
224
|
+
lock.lock()
|
|
225
|
+
defer { lock.unlock() }
|
|
226
|
+
return handle
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// MARK: - Marshalling
|
|
230
|
+
|
|
231
|
+
/// Numbers arrive as `Double` because that is what JavaScript has. The
|
|
232
|
+
/// clamping lives in `LogRotationPolicy.init`, which is also where the Kotlin
|
|
233
|
+
/// side's equivalent has to agree with it.
|
|
234
|
+
private static func policy(from config: RotationConfig?) -> LogRotationPolicy {
|
|
235
|
+
guard let config else { return LogRotationPolicy() }
|
|
236
|
+
return LogRotationPolicy(
|
|
237
|
+
maxFileSizeBytes: config.maxFileSizeBytes,
|
|
238
|
+
maxArchivedFilesCount: config.maxArchivedFilesCount,
|
|
239
|
+
maxFileAgeSeconds: config.maxFileAgeSeconds,
|
|
240
|
+
compressArchives: config.compressArchives,
|
|
241
|
+
maxArchiveAgeSeconds: config.maxArchiveAgeSeconds,
|
|
242
|
+
maxTotalLogBytes: config.maxTotalLogBytes
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private static func status(_ status: LogSinkStatus) -> SinkStatus {
|
|
247
|
+
SinkStatus(
|
|
248
|
+
queuedBytes: Double(status.queuedBytes),
|
|
249
|
+
lostBytes: Double(status.lostBytes),
|
|
250
|
+
lostEntries: Double(status.lostEntries),
|
|
251
|
+
degraded: Double(status.degraded)
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private static func appendResult(_ result: LogAppendResult) -> AppendResult {
|
|
256
|
+
AppendResult(
|
|
257
|
+
accepted: result.accepted,
|
|
258
|
+
rejectReason: result.rejectReason.map(reason),
|
|
259
|
+
queuedBytes: Double(result.status.queuedBytes),
|
|
260
|
+
lostBytes: Double(result.status.lostBytes),
|
|
261
|
+
lostEntries: Double(result.status.lostEntries),
|
|
262
|
+
degraded: Double(result.status.degraded)
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private static func flushOutcome(_ outcome: LogFlushOutcome) -> FlushOutcome {
|
|
267
|
+
FlushOutcome(
|
|
268
|
+
durable: outcome.durable,
|
|
269
|
+
timedOut: outcome.timedOut,
|
|
270
|
+
pendingBytes: Double(outcome.pendingBytes),
|
|
271
|
+
queuedBytes: Double(outcome.status.queuedBytes),
|
|
272
|
+
lostBytes: Double(outcome.status.lostBytes),
|
|
273
|
+
lostEntries: Double(outcome.status.lostEntries),
|
|
274
|
+
degraded: Double(outcome.status.degraded)
|
|
275
|
+
)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private static func emptyOutcome() -> FlushOutcome {
|
|
279
|
+
FlushOutcome(durable: true, timedOut: false, pendingBytes: 0,
|
|
280
|
+
queuedBytes: 0, lostBytes: 0, lostEntries: 0, degraded: 0)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private static func reason(_ reason: LogRejectReason) -> RejectReason {
|
|
284
|
+
switch reason {
|
|
285
|
+
case .full: return .full
|
|
286
|
+
case .staleGeneration: return .stalegeneration
|
|
287
|
+
case .closed: return .closed
|
|
288
|
+
case .failed: return .failed
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import NitroModules
|
|
3
|
+
|
|
4
|
+
/// The Nitro end of the os_log sink: marshalling, and nothing else.
|
|
5
|
+
///
|
|
6
|
+
/// The level map and the chunking live in `NativeConsoleWriter`, which imports
|
|
7
|
+
/// nothing from Nitro and therefore runs under XCTest. Anything that grows a
|
|
8
|
+
/// decision in this file has been put in the wrong place — the same split the
|
|
9
|
+
/// file sink uses, for the same reason.
|
|
10
|
+
final class HybridNativeConsoleSink: HybridNativeConsoleSinkSpec {
|
|
11
|
+
private let writer = NativeConsoleWriter()
|
|
12
|
+
|
|
13
|
+
func install(subsystem: String, category: String) throws {
|
|
14
|
+
writer.install(subsystem: subsystem, category: category)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
func logBatch(levels: [Double], messages: [String]) throws {
|
|
18
|
+
writer.logBatch(levels: levels, messages: messages)
|
|
19
|
+
}
|
|
20
|
+
}
|