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
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
package com.margelo.nitro.nitrologger
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Rotation and retention for one log file.
|
|
5
|
+
*
|
|
6
|
+
* Every field is clamped on the way in. These numbers arrive from JavaScript,
|
|
7
|
+
* where `NaN`, `Infinity` and `-1` are all ordinary values, and an unchecked
|
|
8
|
+
* `Double.toLong()` of any of them is silently wrong rather than loud — `NaN`
|
|
9
|
+
* becomes 0, which would turn "rotate at 10 MB" into "rotate on every write".
|
|
10
|
+
*
|
|
11
|
+
* The clamping rules are the iOS writer's, deliberately: a policy that behaves
|
|
12
|
+
* differently on the two platforms is a bug report nobody can reproduce.
|
|
13
|
+
*/
|
|
14
|
+
data class LogRotationPolicy(
|
|
15
|
+
val maxFileSizeBytes: Long,
|
|
16
|
+
val maxArchivedFilesCount: Int,
|
|
17
|
+
val maxFileAgeSeconds: Double?,
|
|
18
|
+
val compressArchives: Boolean,
|
|
19
|
+
val maxArchiveAgeSeconds: Double?,
|
|
20
|
+
val maxTotalLogBytes: Long?
|
|
21
|
+
) {
|
|
22
|
+
companion object {
|
|
23
|
+
/** Effectively "no limit" while still fitting in the types below. */
|
|
24
|
+
private const val BYTE_CEILING = 1_000_000_000_000_000L
|
|
25
|
+
private const val COUNT_CEILING = 10_000
|
|
26
|
+
private const val SECONDS_CEILING = 1e9
|
|
27
|
+
|
|
28
|
+
const val DEFAULT_MAX_FILE_SIZE_BYTES = 10_485_760L
|
|
29
|
+
const val DEFAULT_MAX_ARCHIVED_FILES = 5
|
|
30
|
+
|
|
31
|
+
fun of(
|
|
32
|
+
maxFileSizeBytes: Double = DEFAULT_MAX_FILE_SIZE_BYTES.toDouble(),
|
|
33
|
+
maxArchivedFilesCount: Double = DEFAULT_MAX_ARCHIVED_FILES.toDouble(),
|
|
34
|
+
maxFileAgeSeconds: Double? = null,
|
|
35
|
+
compressArchives: Boolean = false,
|
|
36
|
+
maxArchiveAgeSeconds: Double? = null,
|
|
37
|
+
maxTotalLogBytes: Double? = null
|
|
38
|
+
): LogRotationPolicy = LogRotationPolicy(
|
|
39
|
+
maxFileSizeBytes = bytes(maxFileSizeBytes, DEFAULT_MAX_FILE_SIZE_BYTES),
|
|
40
|
+
maxArchivedFilesCount = count(maxArchivedFilesCount, DEFAULT_MAX_ARCHIVED_FILES),
|
|
41
|
+
maxFileAgeSeconds = seconds(maxFileAgeSeconds),
|
|
42
|
+
compressArchives = compressArchives,
|
|
43
|
+
maxArchiveAgeSeconds = seconds(maxArchiveAgeSeconds),
|
|
44
|
+
maxTotalLogBytes = optionalBytes(maxTotalLogBytes)
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* `Infinity` clamps UP, to the ceiling. It reads as "never rotate", and
|
|
49
|
+
* folding it into the default — or into zero — would turn a request for no
|
|
50
|
+
* rotation into constant rotation. Only `NaN` and non-positive values fall
|
|
51
|
+
* back, because those carry no intent to honour.
|
|
52
|
+
*/
|
|
53
|
+
private fun bytes(value: Double, fallback: Long): Long = when {
|
|
54
|
+
value.isNaN() || value < 1 -> fallback
|
|
55
|
+
value.isInfinite() -> BYTE_CEILING
|
|
56
|
+
else -> minOf(value, BYTE_CEILING.toDouble()).toLong()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Same rule as [bytes], and for a sharper reason: this limit is what
|
|
61
|
+
* pruning deletes down to.
|
|
62
|
+
*
|
|
63
|
+
* A literal zero is a real instruction — "keep no archives" — so it is
|
|
64
|
+
* honoured. `NaN` and negatives are not instructions at all, and folding
|
|
65
|
+
* them into zero would make one malformed number from JavaScript delete
|
|
66
|
+
* every rotated file on the next sweep. They fall back to the default
|
|
67
|
+
* instead: retaining five archives nobody asked for is recoverable, and
|
|
68
|
+
* deleting the lot is not.
|
|
69
|
+
*/
|
|
70
|
+
private fun count(value: Double, fallback: Int): Int = when {
|
|
71
|
+
value.isNaN() || value < 0 -> fallback
|
|
72
|
+
value.isInfinite() -> COUNT_CEILING
|
|
73
|
+
else -> minOf(value, COUNT_CEILING.toDouble()).toInt()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Optional limits read `Infinity` as absence: "no age cap" and "no cap at
|
|
78
|
+
* all" are the same instruction, and null is how this type spells it.
|
|
79
|
+
*/
|
|
80
|
+
private fun seconds(value: Double?): Double? {
|
|
81
|
+
if (value == null || !value.isFinite() || value <= 0) return null
|
|
82
|
+
return minOf(value, SECONDS_CEILING)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private fun optionalBytes(value: Double?): Long? {
|
|
86
|
+
if (value == null || !value.isFinite() || value < 1) return null
|
|
87
|
+
return minOf(value, BYTE_CEILING.toDouble()).toLong()
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Payload-free record of what has stopped working.
|
|
94
|
+
*
|
|
95
|
+
* A bitmask rather than messages on purpose: these cross into JavaScript and
|
|
96
|
+
* eventually into a log file, and a path or an errno string is exactly the kind
|
|
97
|
+
* of thing that carries a username in it.
|
|
98
|
+
*/
|
|
99
|
+
object LogDegradation {
|
|
100
|
+
const val NONE = 0
|
|
101
|
+
const val ROTATION = 1 shl 0
|
|
102
|
+
const val GZIP = 1 shl 1
|
|
103
|
+
const val PRUNE = 1 shl 2
|
|
104
|
+
const val SIDECAR = 1 shl 3
|
|
105
|
+
const val PROTECTION = 1 shl 4
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
enum class LogRejectReason(val wire: String) {
|
|
109
|
+
FULL("full"),
|
|
110
|
+
STALE_GENERATION("staleGeneration"),
|
|
111
|
+
CLOSED("closed"),
|
|
112
|
+
FAILED("failed")
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
data class LogSinkStatus(
|
|
116
|
+
val queuedBytes: Long,
|
|
117
|
+
val lostBytes: Long,
|
|
118
|
+
val lostEntries: Long,
|
|
119
|
+
val degraded: Int
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
data class LogAppendResult(
|
|
123
|
+
val accepted: Boolean,
|
|
124
|
+
val rejectReason: LogRejectReason?,
|
|
125
|
+
val status: LogSinkStatus
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Numbers arriving from JavaScript, which reach Kotlin as [Double].
|
|
130
|
+
*
|
|
131
|
+
* This lives here rather than in the Nitro adapter so the JUnit suite can
|
|
132
|
+
* reach it: the adapter needs a device, and this is the part worth testing.
|
|
133
|
+
*/
|
|
134
|
+
object BridgeNumber {
|
|
135
|
+
/**
|
|
136
|
+
* [value] as an exact [Long], or null if it was never one.
|
|
137
|
+
*
|
|
138
|
+
* `Double.toLong()` saturates and truncates rather than failing, so every
|
|
139
|
+
* check has to happen before the conversion. NaN would become 0 — which then
|
|
140
|
+
* passes the writer's own checks whenever the batch is empty too — each
|
|
141
|
+
* infinity would become a bound, and 2.5 would become 2, an accepted batch
|
|
142
|
+
* carrying a count nobody sent. iOS refuses all of these via `Int(exactly:)`;
|
|
143
|
+
* this is the same refusal.
|
|
144
|
+
*/
|
|
145
|
+
fun exactLong(value: Double): Long? {
|
|
146
|
+
if (!value.isFinite()) return null
|
|
147
|
+
if (value != Math.floor(value)) return null
|
|
148
|
+
// Integral is not sufficient: 1e19 has no fractional part and still does
|
|
149
|
+
// not fit, and `toLong()` would clamp it to Long.MAX_VALUE.
|
|
150
|
+
//
|
|
151
|
+
// The two bounds are deliberately not symmetric, because the two constants
|
|
152
|
+
// are not. Long.MIN_VALUE is -2^63, exactly representable as a Double, so
|
|
153
|
+
// it converts and the comparison is inclusive. Long.MAX_VALUE is 2^63 - 1,
|
|
154
|
+
// which is NOT representable — `Long.MAX_VALUE.toDouble()` rounds UP to
|
|
155
|
+
// 2^63. An inclusive upper bound would therefore admit 2^63 itself and let
|
|
156
|
+
// `toLong()` saturate it to 2^63 - 1: a silently different number, which is
|
|
157
|
+
// the precise failure this function exists to prevent. Hence `>=`.
|
|
158
|
+
if (value < Long.MIN_VALUE.toDouble() || value >= Long.MAX_VALUE.toDouble()) return null
|
|
159
|
+
return value.toLong()
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
data class LogFlushOutcome(
|
|
164
|
+
val durable: Boolean,
|
|
165
|
+
val timedOut: Boolean,
|
|
166
|
+
val pendingBytes: Long,
|
|
167
|
+
val status: LogSinkStatus
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
data class LogClearOutcome(
|
|
171
|
+
val deletedCount: Int,
|
|
172
|
+
val failedPaths: List<String>,
|
|
173
|
+
/** Every artifact is gone. This is the compliance question. */
|
|
174
|
+
val durable: Boolean,
|
|
175
|
+
/**
|
|
176
|
+
* The writer has a usable file again. Separate from [durable] because a
|
|
177
|
+
* complete deletion can still be followed by a failed reopen, and a caller
|
|
178
|
+
* that resumes on [durable] alone writes into a writer with nowhere to put
|
|
179
|
+
* anything.
|
|
180
|
+
*/
|
|
181
|
+
val rebound: Boolean = false
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
class LogWriterException(val kind: Kind, message: String) : Exception(message) {
|
|
185
|
+
enum class Kind { OPEN_FAILED, CONFIG_CONFLICT, SYMLINK_ESCAPE, STILL_CLOSING }
|
|
186
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
package com.margelo.nitro.nitrologger
|
|
2
|
+
|
|
3
|
+
import java.io.File
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Creating and locking down the files this package writes.
|
|
7
|
+
*
|
|
8
|
+
* The iOS twin has more to do here: a data-protection class and a backup
|
|
9
|
+
* exclusion attribute, both applied per artifact. Android's equivalents are
|
|
10
|
+
* structural rather than per file — the log directory lives under
|
|
11
|
+
* `noBackupFilesDir`, which keeps it out of Auto Backup, and app-private
|
|
12
|
+
* storage is already unreadable by other apps — so what is left for this file
|
|
13
|
+
* is the second layer: owner-only modes on every artifact, and a refusal to
|
|
14
|
+
* follow a symlink out of the directory the registry believes it owns.
|
|
15
|
+
*
|
|
16
|
+
* Every function reports whether it fully succeeded rather than throwing.
|
|
17
|
+
* Failing to tighten a mode is worth recording as a `protection` degradation;
|
|
18
|
+
* it is not worth refusing to log over.
|
|
19
|
+
*/
|
|
20
|
+
object LogSecureFile {
|
|
21
|
+
/**
|
|
22
|
+
* Creates the directory chain and restricts each level to its owner.
|
|
23
|
+
*
|
|
24
|
+
* Each level, not just the leaf: a 0700 directory inside a world-readable one
|
|
25
|
+
* still hides its contents, but the intermediate `logs` name and the
|
|
26
|
+
* timestamps on it are visible, and there is no reason to leak them.
|
|
27
|
+
*/
|
|
28
|
+
fun createDirectory(directory: File, platform: PlatformIo): Boolean {
|
|
29
|
+
var ok = true
|
|
30
|
+
|
|
31
|
+
if (!directory.isDirectory) {
|
|
32
|
+
// Walk down from the highest missing ancestor so each level can be
|
|
33
|
+
// tightened as it appears. `mkdirs()` alone creates the whole chain with
|
|
34
|
+
// default modes and gives no hook in between.
|
|
35
|
+
val missing = generateSequence(directory) { it.parentFile }
|
|
36
|
+
.takeWhile { !it.isDirectory }
|
|
37
|
+
.toList()
|
|
38
|
+
.asReversed()
|
|
39
|
+
|
|
40
|
+
for (level in missing) {
|
|
41
|
+
if (!level.mkdir() && !level.isDirectory) return false
|
|
42
|
+
if (!platform.restrictToOwner(level, isDirectory = true)) ok = false
|
|
43
|
+
}
|
|
44
|
+
} else if (!platform.restrictToOwner(directory, isDirectory = true)) {
|
|
45
|
+
ok = false
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return ok && directory.isDirectory
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Restricts one artifact to its owner. */
|
|
52
|
+
fun secure(file: File, platform: PlatformIo): Boolean {
|
|
53
|
+
if (!file.exists()) return true
|
|
54
|
+
return platform.restrictToOwner(file, isDirectory = file.isDirectory)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Whether the final component of a path is a symlink.
|
|
59
|
+
*
|
|
60
|
+
* Following one would write the app's log wherever the link points — a path
|
|
61
|
+
* the caller never named and the purge would never clean — and would let two
|
|
62
|
+
* different registry keys resolve to the same file. This is the `lstat` +
|
|
63
|
+
* `S_IFLNK` check the iOS writer does, and it deliberately asks about the
|
|
64
|
+
* leaf alone.
|
|
65
|
+
*
|
|
66
|
+
* **Only the leaf.** A symlinked *ancestor* is not a problem to refuse over:
|
|
67
|
+
* the registry resolves the directory canonically before the writer ever
|
|
68
|
+
* sees the path, precisely so that two names for one directory collapse to
|
|
69
|
+
* one key. Comparing `canonicalFile` to `absoluteFile` would fold that
|
|
70
|
+
* intended resolution into the check and refuse to open anything under a
|
|
71
|
+
* symlinked parent at all — which on macOS is every `createTempFile`
|
|
72
|
+
* directory, since `/var` is a link to `/private/var`.
|
|
73
|
+
*
|
|
74
|
+
* Delegated to [PlatformIo] rather than answered here: `Files.isSymbolicLink`
|
|
75
|
+
* is API 26 and this library supports 24, so Android answers it with
|
|
76
|
+
* `Os.lstat` instead.
|
|
77
|
+
*/
|
|
78
|
+
fun isSymbolicLink(file: File, platform: PlatformIo): Boolean =
|
|
79
|
+
platform.isSymbolicLink(file)
|
|
80
|
+
}
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
package com.margelo.nitro.nitrologger
|
|
2
|
+
|
|
3
|
+
import java.io.File
|
|
4
|
+
import java.util.concurrent.TimeUnit
|
|
5
|
+
import java.util.concurrent.locks.ReentrantLock
|
|
6
|
+
import kotlin.concurrent.withLock
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* One writer per file, no matter how many destinations point at it.
|
|
10
|
+
*
|
|
11
|
+
* Two streams appending to the same path from different threads interleave
|
|
12
|
+
* mid-record, and two rotation schedules racing over the same file archive each
|
|
13
|
+
* other's fresh output. The registry makes that impossible by construction:
|
|
14
|
+
* everything that resolves to the same file gets the same [LogFileWriter].
|
|
15
|
+
*
|
|
16
|
+
* **The key is the canonical path.** `logs/app.log`, `logs/../logs/app.log`,
|
|
17
|
+
* and a `logs` symlink into the real directory are the same file and must land
|
|
18
|
+
* on the same entry — comparing the strings the caller passed in would hand out
|
|
19
|
+
* two writers for one file, which is exactly the collision the registry exists
|
|
20
|
+
* to stop.
|
|
21
|
+
*/
|
|
22
|
+
class LogWriterRegistry {
|
|
23
|
+
companion object {
|
|
24
|
+
val shared = LogWriterRegistry()
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* How long an acquire will wait for a previous writer on the same path to
|
|
28
|
+
* finish shutting down before giving up.
|
|
29
|
+
*/
|
|
30
|
+
const val CLOSE_WAIT_MS = 5_000L
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Turns a caller-supplied path into the canonical file it names.
|
|
34
|
+
*
|
|
35
|
+
* The parent directory is created before resolution, because canonical
|
|
36
|
+
* resolution answers only for things that exist — resolving first would
|
|
37
|
+
* fall back to the literal string on a fresh install and hand out a key
|
|
38
|
+
* that stops matching the moment the directory appears.
|
|
39
|
+
*/
|
|
40
|
+
@Throws(LogWriterException::class)
|
|
41
|
+
fun resolve(path: String, platform: PlatformIo): Resolved {
|
|
42
|
+
if (path.isEmpty()) {
|
|
43
|
+
throw LogWriterException(LogWriterException.Kind.OPEN_FAILED, "empty path")
|
|
44
|
+
}
|
|
45
|
+
// Collapse `.` and `..` lexically before anything looks at the parent.
|
|
46
|
+
// `logs/sub/../app.log` names a file in `logs`, but taken literally its
|
|
47
|
+
// parent is `logs/sub/..` — a directory that only exists if `sub` does,
|
|
48
|
+
// so creating it fails and the open dies on a path that was perfectly
|
|
49
|
+
// valid. iOS gets this from `standardizedFileURL`.
|
|
50
|
+
val requested = lexicallyNormalized(File(path).absoluteFile)
|
|
51
|
+
val name = requested.name
|
|
52
|
+
if (name.isEmpty() || name == "." || name == "..") {
|
|
53
|
+
throw LogWriterException(
|
|
54
|
+
LogWriterException.Kind.OPEN_FAILED,
|
|
55
|
+
"path does not name a file"
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
val directory = requested.parentFile
|
|
59
|
+
?: throw LogWriterException(
|
|
60
|
+
LogWriterException.Kind.OPEN_FAILED,
|
|
61
|
+
"path has no directory"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
if (!LogSecureFile.createDirectory(directory, platform) && !directory.isDirectory) {
|
|
65
|
+
throw LogWriterException(
|
|
66
|
+
LogWriterException.Kind.OPEN_FAILED,
|
|
67
|
+
"could not create the log directory"
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
val canonicalDirectory = try {
|
|
72
|
+
directory.canonicalFile
|
|
73
|
+
} catch (_: Exception) {
|
|
74
|
+
throw LogWriterException(
|
|
75
|
+
LogWriterException.Kind.OPEN_FAILED,
|
|
76
|
+
"could not resolve the log directory"
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// The log file itself must not be a symlink, even though the directory
|
|
81
|
+
// above it was resolved through one deliberately.
|
|
82
|
+
val candidate = File(canonicalDirectory, name)
|
|
83
|
+
if (LogSecureFile.isSymbolicLink(candidate, platform)) {
|
|
84
|
+
throw LogWriterException(
|
|
85
|
+
LogWriterException.Kind.SYMLINK_ESCAPE,
|
|
86
|
+
"the log file is a symbolic link"
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return Resolved(candidate, candidate.absolutePath)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Collapses `.` and `..` in an absolute path, textually.
|
|
95
|
+
*
|
|
96
|
+
* `Path.normalize()` would be the obvious call and was the original one, but
|
|
97
|
+
* `java.nio.file` arrived in API 26 and this library supports 24. A missing
|
|
98
|
+
* class there raises `NoClassDefFoundError` — an `Error`, which passes
|
|
99
|
+
* straight through `catch (Exception)` — so on an older device the failure
|
|
100
|
+
* would not have been a fallback but an unhandled throw out of `open()`.
|
|
101
|
+
*
|
|
102
|
+
* Purely lexical, like the original: it does not touch the filesystem, so
|
|
103
|
+
* `a/symlink/../b` collapses to `a/b` whether or not that is where the link
|
|
104
|
+
* pointed. That is the same trade `standardizedFileURL` makes on iOS, and it
|
|
105
|
+
* is safe here because the *directory* is canonicalised for real straight
|
|
106
|
+
* afterwards, and the leaf is `lstat`-checked.
|
|
107
|
+
*/
|
|
108
|
+
private fun lexicallyNormalized(file: File): File {
|
|
109
|
+
val separator = File.separatorChar
|
|
110
|
+
val stack = ArrayList<String>()
|
|
111
|
+
for (part in file.absolutePath.split(separator)) {
|
|
112
|
+
when (part) {
|
|
113
|
+
"", "." -> Unit
|
|
114
|
+
".." -> if (stack.isNotEmpty()) stack.removeAt(stack.size - 1)
|
|
115
|
+
else -> stack.add(part)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return File(separator + stack.joinToString(separator.toString()))
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** A registry with no shared state, so tests do not leak writers. */
|
|
122
|
+
fun isolated() = LogWriterRegistry()
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
data class Resolved(val file: File, val canonicalPath: String)
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* A lock with a condition, because acquisition sometimes has to *wait* — see
|
|
129
|
+
* [closing].
|
|
130
|
+
*/
|
|
131
|
+
private val lock = ReentrantLock()
|
|
132
|
+
private val pathFreed = lock.newCondition()
|
|
133
|
+
private val writers = HashMap<String, LogFileWriter>()
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Paths whose writer has been evicted but is still draining and closing.
|
|
137
|
+
*
|
|
138
|
+
* Eviction and close cannot be one atomic step: closing waits on the write
|
|
139
|
+
* executor, and holding the registry lock across that would stall every other
|
|
140
|
+
* file. But between the two, the map has no entry for the path — so an
|
|
141
|
+
* acquire arriving in that window would build a *second* writer while the
|
|
142
|
+
* first still has accepted batches queued, giving one file two executors and
|
|
143
|
+
* two rotation schedules. That is the exact collision this registry exists to
|
|
144
|
+
* prevent, so acquisition waits out the close instead.
|
|
145
|
+
*
|
|
146
|
+
* Counted rather than a set: it costs nothing and stops a stray double
|
|
147
|
+
* release from clearing a marker another close still needs.
|
|
148
|
+
*/
|
|
149
|
+
private val closing = HashMap<String, Int>()
|
|
150
|
+
private var nextHandleId = 1L
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Acquires a handle on the writer for [path], creating it if needed.
|
|
154
|
+
*
|
|
155
|
+
* Acquisition happens entirely under the lock, so two runtimes opening the
|
|
156
|
+
* same file concurrently cannot both construct a writer and have one silently
|
|
157
|
+
* replace the other.
|
|
158
|
+
*/
|
|
159
|
+
@Throws(LogWriterException::class)
|
|
160
|
+
fun acquire(
|
|
161
|
+
path: String,
|
|
162
|
+
policy: LogRotationPolicy,
|
|
163
|
+
lineFramed: Boolean,
|
|
164
|
+
/**
|
|
165
|
+
* Required, with no default on purpose — see [LogFileWriter.open]. Falling
|
|
166
|
+
* back to [PlatformIo.Jvm] on Android would put `java.nio.file` back on the
|
|
167
|
+
* open path, where it does not exist below API 26.
|
|
168
|
+
*/
|
|
169
|
+
platform: PlatformIo,
|
|
170
|
+
rawWrite: LogFileWriter.RawWrite? = null,
|
|
171
|
+
compressor: LogFileWriter.Compressor? = null,
|
|
172
|
+
clock: (() -> Long)? = null,
|
|
173
|
+
monotonic: (() -> Long)? = null
|
|
174
|
+
): LogFileHandle {
|
|
175
|
+
val resolved = resolve(path, platform)
|
|
176
|
+
|
|
177
|
+
lock.withLock {
|
|
178
|
+
// Wait out a close still in progress on this path — but not forever. The
|
|
179
|
+
// claim is cleared by the writer's own executor rather than by whoever
|
|
180
|
+
// called close, so wedged storage means it may never clear at all.
|
|
181
|
+
// Failing the open is the fail-closed answer: one writer per file is the
|
|
182
|
+
// invariant worth keeping, and a caller that cannot have it should be
|
|
183
|
+
// told so rather than handed a second one.
|
|
184
|
+
var budget = CLOSE_WAIT_MS
|
|
185
|
+
while (closing.containsKey(resolved.canonicalPath)) {
|
|
186
|
+
if (budget <= 0) {
|
|
187
|
+
throw LogWriterException(
|
|
188
|
+
LogWriterException.Kind.STILL_CLOSING,
|
|
189
|
+
"a previous writer for this path is still closing"
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
val waited = try {
|
|
193
|
+
pathFreed.awaitNanos(TimeUnit.MILLISECONDS.toNanos(budget))
|
|
194
|
+
} catch (_: InterruptedException) {
|
|
195
|
+
// Same answer as running out of budget, and for the same reason: one
|
|
196
|
+
// writer per file is the invariant, and a caller that cannot be given
|
|
197
|
+
// it is told so rather than handed a second one.
|
|
198
|
+
Thread.currentThread().interrupt()
|
|
199
|
+
throw LogWriterException(
|
|
200
|
+
LogWriterException.Kind.STILL_CLOSING,
|
|
201
|
+
"interrupted while a previous writer for this path was closing"
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
budget = TimeUnit.NANOSECONDS.toMillis(waited)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
val existing = writers[resolved.canonicalPath]
|
|
208
|
+
val writer: LogFileWriter
|
|
209
|
+
if (existing != null && !existing.isClosed) {
|
|
210
|
+
// A second destination on the same file must agree about how that file
|
|
211
|
+
// is written. Silently honouring the first caller's rotation policy
|
|
212
|
+
// would give the second one a file that behaves nothing like what it
|
|
213
|
+
// asked for, and silently honouring the last would change it under the
|
|
214
|
+
// first.
|
|
215
|
+
if (existing.policy != policy || existing.lineFramed != lineFramed) {
|
|
216
|
+
throw LogWriterException(
|
|
217
|
+
LogWriterException.Kind.CONFIG_CONFLICT,
|
|
218
|
+
"this file is already open with a different configuration"
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
writer = existing
|
|
222
|
+
} else {
|
|
223
|
+
writer = LogFileWriter.open(
|
|
224
|
+
file = resolved.file,
|
|
225
|
+
canonicalPath = resolved.canonicalPath,
|
|
226
|
+
policy = policy,
|
|
227
|
+
lineFramed = lineFramed,
|
|
228
|
+
platform = platform,
|
|
229
|
+
rawWrite = rawWrite,
|
|
230
|
+
compressor = compressor,
|
|
231
|
+
clock = clock,
|
|
232
|
+
monotonic = monotonic
|
|
233
|
+
)
|
|
234
|
+
writers[resolved.canonicalPath] = writer
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
writer.retain()
|
|
238
|
+
val id = nextHandleId
|
|
239
|
+
nextHandleId += 1
|
|
240
|
+
return LogFileHandle(id, writer, this)
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Drops one handle's claim, closing and evicting the writer at zero.
|
|
246
|
+
*/
|
|
247
|
+
internal fun release(writer: LogFileWriter, handleId: Long, deadlineMs: Double) {
|
|
248
|
+
val path = writer.canonicalPath
|
|
249
|
+
|
|
250
|
+
val shouldClose = lock.withLock {
|
|
251
|
+
val remaining = writer.releaseOne()
|
|
252
|
+
if (remaining > 0) return@withLock false
|
|
253
|
+
if (writers[path] === writer) writers.remove(path)
|
|
254
|
+
// Claim the path for the duration of the close. An acquire arriving now
|
|
255
|
+
// waits rather than building a rival writer over a file this one is still
|
|
256
|
+
// draining.
|
|
257
|
+
closing[path] = (closing[path] ?: 0) + 1
|
|
258
|
+
true
|
|
259
|
+
}
|
|
260
|
+
if (!shouldClose) return
|
|
261
|
+
|
|
262
|
+
// The claim is dropped by the writer's own executor, not when this call
|
|
263
|
+
// stops waiting. A close that hits its deadline leaves work still
|
|
264
|
+
// executing; releasing the path then would let a replacement writer open
|
|
265
|
+
// the same file underneath it.
|
|
266
|
+
writer.close(handleId, deadlineMs) { finishClosing(path) }
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private fun finishClosing(path: String) {
|
|
270
|
+
lock.withLock {
|
|
271
|
+
val outstanding = closing[path] ?: 0
|
|
272
|
+
if (outstanding > 1) closing[path] = outstanding - 1 else closing.remove(path)
|
|
273
|
+
pathFreed.signalAll()
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
val liveWriterCountForTesting: Int get() = lock.withLock { writers.size }
|
|
278
|
+
val closingCountForTesting: Int get() = lock.withLock { closing.size }
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* One destination's claim on a writer.
|
|
283
|
+
*
|
|
284
|
+
* The generation is the fence. A purge bumps the writer's generation; a handle
|
|
285
|
+
* that has not rebound to the new one is refused with `staleGeneration` rather
|
|
286
|
+
* than being allowed to write pre-purge data into the fresh file.
|
|
287
|
+
*/
|
|
288
|
+
class LogFileHandle internal constructor(
|
|
289
|
+
val id: Long,
|
|
290
|
+
private val writer: LogFileWriter,
|
|
291
|
+
private val registry: LogWriterRegistry
|
|
292
|
+
) {
|
|
293
|
+
private enum class State { ACTIVE, CLOSING, CLOSED }
|
|
294
|
+
|
|
295
|
+
private val lock = ReentrantLock()
|
|
296
|
+
private val purgeFinished = lock.newCondition()
|
|
297
|
+
private var generation = writer.currentGeneration
|
|
298
|
+
private var state = State.ACTIVE
|
|
299
|
+
/** A purge is running on this handle; close has to wait it out. */
|
|
300
|
+
private var purging = false
|
|
301
|
+
|
|
302
|
+
val filePath: String get() = writer.file.absolutePath
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Whether this handle may still speak for the writer.
|
|
306
|
+
*
|
|
307
|
+
* Every entry point below is gated on it, and on ACTIVE specifically rather
|
|
308
|
+
* than "not yet CLOSED". A handle in CLOSING has already had its flush
|
|
309
|
+
* barrier enqueued by [close]; letting it submit more work, or report a
|
|
310
|
+
* status it will not be around to make true, is how a released destination
|
|
311
|
+
* ends up observing — or writing to — a writer that another handle now owns.
|
|
312
|
+
*/
|
|
313
|
+
private val isLive: Boolean get() = lock.withLock { state == State.ACTIVE }
|
|
314
|
+
|
|
315
|
+
private fun inertStatus() = LogSinkStatus(0, 0, 0, 0)
|
|
316
|
+
|
|
317
|
+
fun appendBatch(batch: String, entryCount: Long): LogAppendResult = lock.withLock {
|
|
318
|
+
if (state != State.ACTIVE) {
|
|
319
|
+
return LogAppendResult(false, LogRejectReason.CLOSED, inertStatus())
|
|
320
|
+
}
|
|
321
|
+
writer.append(id, generation, batch, entryCount)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
fun status(): LogSinkStatus {
|
|
325
|
+
if (!isLive) return inertStatus()
|
|
326
|
+
return writer.status(id)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
fun flush(deadlineMs: Double): LogFlushOutcome {
|
|
330
|
+
// Not `durable = true`. A released handle flushed nothing, and saying
|
|
331
|
+
// otherwise invites the caller to treat its pending records as safe.
|
|
332
|
+
if (!isLive) return LogFlushOutcome(false, false, 0, inertStatus())
|
|
333
|
+
return writer.flush(id, deadlineMs)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
fun logFilePaths(): List<String> {
|
|
337
|
+
if (!isLive) return emptyList()
|
|
338
|
+
return writer.logFilePaths()
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Purges, then rebinds only if the writer really came back.
|
|
343
|
+
*
|
|
344
|
+
* `rebound` is a fact about THIS handle, not about the writer: if this handle
|
|
345
|
+
* is closing, or the purge did not end with a usable file, the caller must
|
|
346
|
+
* stay fenced even though the deletion itself may have been perfectly
|
|
347
|
+
* durable.
|
|
348
|
+
*/
|
|
349
|
+
fun clearLogs(deadlineMs: Double): LogClearOutcome {
|
|
350
|
+
lock.lock()
|
|
351
|
+
if (state != State.ACTIVE || purging) {
|
|
352
|
+
lock.unlock()
|
|
353
|
+
return LogClearOutcome(0, listOf(filePath), durable = false)
|
|
354
|
+
}
|
|
355
|
+
purging = true
|
|
356
|
+
lock.unlock()
|
|
357
|
+
|
|
358
|
+
val result = try {
|
|
359
|
+
writer.clearLogs(deadlineMs)
|
|
360
|
+
} finally {
|
|
361
|
+
lock.withLock {
|
|
362
|
+
purging = false
|
|
363
|
+
purgeFinished.signalAll()
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
lock.withLock {
|
|
368
|
+
var outcome = result.first
|
|
369
|
+
if (state == State.ACTIVE && outcome.durable && outcome.rebound) {
|
|
370
|
+
generation = result.second
|
|
371
|
+
} else {
|
|
372
|
+
outcome = outcome.copy(rebound = false)
|
|
373
|
+
}
|
|
374
|
+
return outcome
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Closes this handle, waiting out any purge still running on it.
|
|
380
|
+
*
|
|
381
|
+
* One budget across all three waits. Giving each the full deadline means a
|
|
382
|
+
* caller asking for 200 ms can wait 600.
|
|
383
|
+
*/
|
|
384
|
+
fun close(deadlineMs: Double): LogFlushOutcome {
|
|
385
|
+
// Monotonic, not `currentTimeMillis`. This budget spans three waits, and a
|
|
386
|
+
// device clock that steps backward part-way through — an NTP correction, a
|
|
387
|
+
// user changing the date — would hand the flush and teardown far more than
|
|
388
|
+
// the caller asked for. On the crash path that is the difference between a
|
|
389
|
+
// bounded close and an ANR.
|
|
390
|
+
val expiry = monotonicMs() + LogFileWriter.clampDeadline(deadlineMs)
|
|
391
|
+
|
|
392
|
+
lock.lock()
|
|
393
|
+
if (state != State.ACTIVE) {
|
|
394
|
+
lock.unlock()
|
|
395
|
+
return LogFlushOutcome(false, false, 0, inertStatus())
|
|
396
|
+
}
|
|
397
|
+
state = State.CLOSING
|
|
398
|
+
while (purging) {
|
|
399
|
+
val left = maxOf(0L, expiry - monotonicMs())
|
|
400
|
+
if (left <= 0) break
|
|
401
|
+
try {
|
|
402
|
+
if (!purgeFinished.await(left, TimeUnit.MILLISECONDS)) break
|
|
403
|
+
} catch (_: InterruptedException) {
|
|
404
|
+
Thread.currentThread().interrupt()
|
|
405
|
+
break
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
lock.unlock()
|
|
409
|
+
|
|
410
|
+
val outcome = writer.flush(id, remaining(expiry))
|
|
411
|
+
registry.release(writer, id, remaining(expiry))
|
|
412
|
+
lock.withLock { state = State.CLOSED }
|
|
413
|
+
return outcome
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
private fun remaining(expiry: Long): Double =
|
|
417
|
+
maxOf(0L, expiry - monotonicMs()).toDouble()
|
|
418
|
+
|
|
419
|
+
private fun monotonicMs(): Long = System.nanoTime() / 1_000_000L
|
|
420
|
+
}
|