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,237 @@
|
|
|
1
|
+
package com.margelo.nitro.nitrologger
|
|
2
|
+
|
|
3
|
+
import android.util.Log
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The logcat bridge, minus Nitro.
|
|
7
|
+
*
|
|
8
|
+
* Everything worth getting wrong here — the level map, and what happens to a
|
|
9
|
+
* line longer than logcat will store — is a pure function of its input, so it
|
|
10
|
+
* lives where JUnit can reach it. [HybridNativeConsoleSink] is the adapter
|
|
11
|
+
* around this and holds no logic of its own. The iOS twin,
|
|
12
|
+
* `NativeConsoleWriter.swift`, is split the same way for the same reason.
|
|
13
|
+
*
|
|
14
|
+
* [emit] exists so the tests can watch what would have gone to logcat.
|
|
15
|
+
* `android.util.Log` is a stub under unit tests that swallows everything and
|
|
16
|
+
* returns zero, so a writer that called it directly could only ever be tested
|
|
17
|
+
* by not testing it.
|
|
18
|
+
*/
|
|
19
|
+
class NativeConsoleWriter(private val emit: (Int, String, String) -> Unit = LOGCAT) {
|
|
20
|
+
|
|
21
|
+
@Volatile
|
|
22
|
+
private var tag: String = FALLBACK_TAG
|
|
23
|
+
|
|
24
|
+
/** Binds the tag. Calling it again rebinds. */
|
|
25
|
+
fun install(subsystem: String, category: String) {
|
|
26
|
+
// logcat has no subsystem dimension, so the category becomes the tag and
|
|
27
|
+
// the subsystem is deliberately unused — it is already in the rendered
|
|
28
|
+
// line. An empty category would produce entries nobody can filter for.
|
|
29
|
+
tag = category.ifEmpty { FALLBACK_TAG }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Writes one drained batch.
|
|
34
|
+
*
|
|
35
|
+
* `levels` and `messages` are parallel arrays rather than an array of
|
|
36
|
+
* structs, which keeps a batch to two bridge crossings instead of one per
|
|
37
|
+
* entry. `messages` is authoritative on count: a short `levels` means the
|
|
38
|
+
* caller has a bug, and dropping the messages it does have would turn that
|
|
39
|
+
* bug into missing logs.
|
|
40
|
+
*/
|
|
41
|
+
fun logBatch(levels: DoubleArray, messages: Array<String>) {
|
|
42
|
+
if (messages.isEmpty()) return
|
|
43
|
+
val tag = this.tag
|
|
44
|
+
for (i in messages.indices) {
|
|
45
|
+
val priority = priorityOf(if (i < levels.size) levels[i] else 2.0)
|
|
46
|
+
for (chunk in chunks(messages[i])) emit(priority, tag, chunk)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
companion object {
|
|
51
|
+
/**
|
|
52
|
+
* Bytes of message text per logcat entry.
|
|
53
|
+
*
|
|
54
|
+
* logcat's per-entry payload is about 4 KB and the rest is dropped in
|
|
55
|
+
* silence — no ellipsis, no diagnostic, the tail simply is not there. For a
|
|
56
|
+
* logger that is the worst kind of failure: the console shows a complete-
|
|
57
|
+
* looking line that is missing the half you needed. 3800 leaves room under
|
|
58
|
+
* the limit for the tag, the priority byte and logcat's own framing.
|
|
59
|
+
*
|
|
60
|
+
* Lower than the iOS 900 because the two platforms' limits genuinely
|
|
61
|
+
* differ; the *behaviour* around the limit is what is kept identical.
|
|
62
|
+
*/
|
|
63
|
+
const val CHUNK_BYTES = 3800
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* At most this many chunks per line. A stack trace should arrive whole; a
|
|
67
|
+
* runaway 4 MB string should not become a thousand logcat entries.
|
|
68
|
+
*/
|
|
69
|
+
const val MAX_CHUNKS = 8
|
|
70
|
+
|
|
71
|
+
/** Used when nothing installed a category, so entries stay findable. */
|
|
72
|
+
const val FALLBACK_TAG = "NitroLogger"
|
|
73
|
+
|
|
74
|
+
private val LOGCAT: (Int, String, String) -> Unit = { priority, tag, message ->
|
|
75
|
+
Log.println(priority, tag, message)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Codes 0–5 (verbose…todo), matching `LEVEL_ORDER` in TypeScript.
|
|
80
|
+
*
|
|
81
|
+
* Matched on the `Double` rather than converted first. Levels arrive as
|
|
82
|
+
* `Double` because that is what JavaScript has, and `toInt()` does not
|
|
83
|
+
* report a bad one — it saturates, so `NaN` becomes 0 and would log a
|
|
84
|
+
* corrupt level as `VERBOSE`, the one priority most likely to be filtered
|
|
85
|
+
* out entirely.
|
|
86
|
+
*
|
|
87
|
+
* A near-integer code is *rounded*, not rejected: 3.6 is a level 4 that
|
|
88
|
+
* survived a float round trip, and iOS has always read it that way. Only a
|
|
89
|
+
* code that rounds to nothing in 0–5 — negative, enormous, `NaN` — is
|
|
90
|
+
* `INFO`. It carries no trustworthy severity, so it is shown rather than
|
|
91
|
+
* hidden as `VERBOSE` or escalated to `ASSERT`. The rendered line still
|
|
92
|
+
* carries its own level tag either way. iOS answers `.default` here.
|
|
93
|
+
*
|
|
94
|
+
* Not `Math.round`, which is half-*up*: it rounds -0.5 to 0 and -1.5 to -1,
|
|
95
|
+
* where Swift's `.rounded()` is half-*away-from-zero* and answers -1 and -2.
|
|
96
|
+
* The two agree on everything except a negative tie, and a level map that
|
|
97
|
+
* disagrees across platforms on any input is the kind of difference nobody
|
|
98
|
+
* can reproduce from a bug report.
|
|
99
|
+
*/
|
|
100
|
+
fun priorityOf(code: Double): Int {
|
|
101
|
+
if (code.isNaN()) return Log.INFO
|
|
102
|
+
return when (roundHalfAwayFromZero(code)) {
|
|
103
|
+
0.0 -> Log.VERBOSE
|
|
104
|
+
1.0 -> Log.DEBUG
|
|
105
|
+
2.0 -> Log.INFO
|
|
106
|
+
3.0 -> Log.WARN
|
|
107
|
+
4.0 -> Log.ERROR
|
|
108
|
+
5.0 -> Log.ASSERT // todo → highest visibility, mirrors os_log .fault
|
|
109
|
+
else -> Log.INFO
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Swift's default rounding rule, which this has to match exactly.
|
|
115
|
+
*
|
|
116
|
+
* Written as "is the fraction at least a half" rather than the usual
|
|
117
|
+
* `floor(v + 0.5)`, because that shortcut is wrong just below every tie:
|
|
118
|
+
* `0.49999999999999994 + 0.5` is not representable and rounds *up* to
|
|
119
|
+
* exactly `1.0`, so the floor answers 1 where Swift answers 0. Comparing
|
|
120
|
+
* the fraction never adds across the boundary and so cannot invent one.
|
|
121
|
+
*
|
|
122
|
+
* Infinities survive as infinities and so match no level, which is what
|
|
123
|
+
* sends them to `INFO`. Above 2^52 every double is already an integer, the
|
|
124
|
+
* fraction is zero, and this returns the value unchanged.
|
|
125
|
+
*/
|
|
126
|
+
private fun roundHalfAwayFromZero(value: Double): Double {
|
|
127
|
+
val magnitude = Math.abs(value)
|
|
128
|
+
val lower = Math.floor(magnitude)
|
|
129
|
+
val rounded = if (magnitude - lower >= 0.5) lower + 1.0 else lower
|
|
130
|
+
return Math.copySign(rounded, value)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Splits a line into pieces logcat will store whole.
|
|
135
|
+
*
|
|
136
|
+
* A line that already fits comes back untouched and unmarked — the common
|
|
137
|
+
* case must look exactly as it was rendered. Only when splitting is
|
|
138
|
+
* unavoidable does each piece get an `(i/n)` prefix, so a reader can tell a
|
|
139
|
+
* continuation from a fresh entry and can see when one is missing.
|
|
140
|
+
*
|
|
141
|
+
* Splits fall on code point boundaries, never inside a surrogate pair:
|
|
142
|
+
* cutting an emoji in half produces replacement characters in logcat and,
|
|
143
|
+
* worse, suggests corruption that is not there. This is a shade weaker than
|
|
144
|
+
* the iOS twin, which splits on grapheme clusters and so also keeps
|
|
145
|
+
* combining sequences together — reaching that here means `BreakIterator`
|
|
146
|
+
* and an ICU version that varies by device, for a case that renders
|
|
147
|
+
* oddly rather than corruptly. `docs/PARITY.md` records the difference.
|
|
148
|
+
*
|
|
149
|
+
* A single code point wider than a whole chunk cannot occur — four bytes at
|
|
150
|
+
* most, against a budget in the thousands.
|
|
151
|
+
*/
|
|
152
|
+
fun chunks(message: String): List<String> {
|
|
153
|
+
if (utf8Length(message) <= CHUNK_BYTES) return listOf(message)
|
|
154
|
+
|
|
155
|
+
// Reserve room for the widest marker any of these pieces can carry, so
|
|
156
|
+
// adding the prefix cannot push a piece back over the limit.
|
|
157
|
+
val budget = CHUNK_BYTES - MARKER_WIDTH
|
|
158
|
+
val pieces = mutableListOf<String>()
|
|
159
|
+
val current = StringBuilder()
|
|
160
|
+
var currentBytes = 0
|
|
161
|
+
|
|
162
|
+
var i = 0
|
|
163
|
+
while (i < message.length) {
|
|
164
|
+
val codePoint = message.codePointAt(i)
|
|
165
|
+
val charCount = Character.charCount(codePoint)
|
|
166
|
+
val width = utf8Width(codePoint)
|
|
167
|
+
|
|
168
|
+
if (currentBytes + width > budget && current.isNotEmpty()) {
|
|
169
|
+
pieces.add(current.toString())
|
|
170
|
+
current.setLength(0)
|
|
171
|
+
currentBytes = 0
|
|
172
|
+
if (pieces.size == MAX_CHUNKS) break
|
|
173
|
+
}
|
|
174
|
+
current.appendRange(message, i, i + charCount)
|
|
175
|
+
currentBytes += width
|
|
176
|
+
i += charCount
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (pieces.size < MAX_CHUNKS && current.isNotEmpty()) pieces.add(current.toString())
|
|
180
|
+
|
|
181
|
+
// Whatever is left over after the chunk ceiling is announced by size
|
|
182
|
+
// rather than dropped in silence. A byte count is a length, not content,
|
|
183
|
+
// so it says nothing about what was in the line.
|
|
184
|
+
var remaining = utf8Length(message) - pieces.sumOf { utf8Length(it) }
|
|
185
|
+
if (remaining > 0 && pieces.isNotEmpty()) {
|
|
186
|
+
val last = StringBuilder(pieces.removeAt(pieces.size - 1))
|
|
187
|
+
// Make room for the notice inside the entry rather than appending past
|
|
188
|
+
// the limit — otherwise logcat truncates the very sentence that exists
|
|
189
|
+
// to report truncation, which is the one line here that must survive.
|
|
190
|
+
// Each code point given back grows the count it reports.
|
|
191
|
+
while (utf8Length(last) + NOTICE_WIDTH + MARKER_WIDTH > CHUNK_BYTES && last.isNotEmpty()) {
|
|
192
|
+
val dropped = last.codePointBefore(last.length)
|
|
193
|
+
last.setLength(last.length - Character.charCount(dropped))
|
|
194
|
+
remaining += utf8Width(dropped)
|
|
195
|
+
}
|
|
196
|
+
last.append(" …+$remaining bytes truncated")
|
|
197
|
+
pieces.add(last.toString())
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
val total = pieces.size
|
|
201
|
+
return pieces.mapIndexed { index, piece -> "(${index + 1}/$total) $piece" }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** `"(8/8) "` — the widest prefix [MAX_CHUNKS] can produce. */
|
|
205
|
+
private val MARKER_WIDTH = utf8Length("($MAX_CHUNKS/$MAX_CHUNKS) ")
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* The truncation notice at its widest, so reserving this much is always
|
|
209
|
+
* enough however large the count turns out to be.
|
|
210
|
+
*/
|
|
211
|
+
private val NOTICE_WIDTH = utf8Length(" …+${Int.MAX_VALUE} bytes truncated")
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* UTF-8 byte length, computed rather than measured.
|
|
215
|
+
*
|
|
216
|
+
* `toByteArray()` would encode the whole string to answer a question about
|
|
217
|
+
* its length, on every code point of every oversized line.
|
|
218
|
+
*/
|
|
219
|
+
private fun utf8Length(text: CharSequence): Int {
|
|
220
|
+
var bytes = 0
|
|
221
|
+
var i = 0
|
|
222
|
+
while (i < text.length) {
|
|
223
|
+
val codePoint = Character.codePointAt(text, i)
|
|
224
|
+
bytes += utf8Width(codePoint)
|
|
225
|
+
i += Character.charCount(codePoint)
|
|
226
|
+
}
|
|
227
|
+
return bytes
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private fun utf8Width(codePoint: Int): Int = when {
|
|
231
|
+
codePoint < 0x80 -> 1
|
|
232
|
+
codePoint < 0x800 -> 2
|
|
233
|
+
codePoint < 0x10000 -> 3
|
|
234
|
+
else -> 4
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
package com.margelo.nitro.nitrologger
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.BaseReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.module.model.ReactModuleInfoProvider
|
|
7
|
+
|
|
8
|
+
class NitroLoggerPackage : BaseReactPackage() {
|
|
9
|
+
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
|
|
10
|
+
return null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
|
|
14
|
+
return ReactModuleInfoProvider { HashMap() }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
companion object {
|
|
18
|
+
init {
|
|
19
|
+
System.loadLibrary("nitrologger")
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
package com.margelo.nitro.nitrologger
|
|
2
|
+
|
|
3
|
+
import java.io.File
|
|
4
|
+
import java.io.FileDescriptor
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The handful of operations the writer needs that `java.io.File` cannot answer.
|
|
8
|
+
*
|
|
9
|
+
* Four of them are load-bearing rather than nice to have:
|
|
10
|
+
*
|
|
11
|
+
* - **Link count.** Writes to an unlinked file succeed forever and land
|
|
12
|
+
* nowhere. `File.exists()` cannot see it — the name may have been recreated
|
|
13
|
+
* by something else while this descriptor still points at the orphan — so
|
|
14
|
+
* without `fstat` an externally deleted log means silent loss for the rest of
|
|
15
|
+
* the process's life.
|
|
16
|
+
* - **Directory sync.** `delete()` returning true only means the change is in
|
|
17
|
+
* the directory's in-memory state. Until the directory itself is synced a
|
|
18
|
+
* crash can bring every one of those names back, and a compliance purge
|
|
19
|
+
* exists precisely to promise they are gone.
|
|
20
|
+
* - **Tri-state lookup.** `File.exists()` returns false for "not there" and for
|
|
21
|
+
* "could not tell" alike. A purge that treats the second as the first claims
|
|
22
|
+
* `durable = true` over artifacts that are still on disk.
|
|
23
|
+
* - **Symlink detection.** `FileOutputStream` has no `O_NOFOLLOW`, so the check
|
|
24
|
+
* has to be a separate `lstat`.
|
|
25
|
+
*
|
|
26
|
+
* They live behind an interface so [LogFileWriter] imports nothing from
|
|
27
|
+
* `android.*` and can be tested on a plain JVM, the same reason the iOS writer
|
|
28
|
+
* has no Nitro imports.
|
|
29
|
+
*
|
|
30
|
+
* **`java.nio.file` is API 26 and this library supports 24.** Every use of it
|
|
31
|
+
* is confined to [Jvm], which runs only under unit tests; [AndroidPlatformIo]
|
|
32
|
+
* implements all four with `Os` syscalls and never delegates here for them. An
|
|
33
|
+
* unresolvable class raises `NoClassDefFoundError`, which is an `Error` and
|
|
34
|
+
* passes straight through `catch (Exception)` — so the isolation is what makes
|
|
35
|
+
* this safe, not the catch blocks.
|
|
36
|
+
*/
|
|
37
|
+
interface PlatformIo {
|
|
38
|
+
/**
|
|
39
|
+
* What a no-follow lookup could establish about a path.
|
|
40
|
+
*
|
|
41
|
+
* [UNKNOWN] is the reason this is not a `Boolean`: a purge may only count an
|
|
42
|
+
* artifact as gone when the platform actually said it was.
|
|
43
|
+
*/
|
|
44
|
+
enum class Presence { PRESENT, ABSENT, UNKNOWN }
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Hard links to the file behind this descriptor, or -1 when unknowable.
|
|
48
|
+
*
|
|
49
|
+
* -1 is not zero: a caller must treat it as "no evidence of a problem"
|
|
50
|
+
* rather than as a deletion, or every write on a platform without `fstat`
|
|
51
|
+
* would look like it had lost its file.
|
|
52
|
+
*/
|
|
53
|
+
fun linkCount(descriptor: FileDescriptor): Int
|
|
54
|
+
|
|
55
|
+
/** Commits a directory's own metadata — the removals — to storage. */
|
|
56
|
+
fun syncDirectory(directory: File): Boolean
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Whether [file] exists, without following a final symlink.
|
|
60
|
+
*
|
|
61
|
+
* Returns [Presence.UNKNOWN] for any failure that is not a plain "no such
|
|
62
|
+
* file": a permissions problem, an I/O error, a volume that went away. The
|
|
63
|
+
* purge reports those as failures rather than as successful deletions.
|
|
64
|
+
*/
|
|
65
|
+
fun lookup(file: File): Presence
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Whether the final component of [file] is a symbolic link.
|
|
69
|
+
*
|
|
70
|
+
* Returns true when it cannot be established. Refusing costs a log file;
|
|
71
|
+
* guessing wrong writes one wherever the link points — a path the caller
|
|
72
|
+
* never named and the purge would never clean.
|
|
73
|
+
*/
|
|
74
|
+
fun isSymbolicLink(file: File): Boolean
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Restricts an artifact to its owner: 0700 for directories, 0600 for files.
|
|
78
|
+
*
|
|
79
|
+
* Returns false if any part did not take, which the writer records as a
|
|
80
|
+
* `protection` degradation rather than a failure. Android's app-private
|
|
81
|
+
* storage is already inaccessible to other apps; these modes are the second
|
|
82
|
+
* layer, and losing them is worth reporting but not worth refusing to log
|
|
83
|
+
* over.
|
|
84
|
+
*/
|
|
85
|
+
fun restrictToOwner(file: File, isDirectory: Boolean): Boolean
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Creation time in epoch millis, or null when the platform cannot say.
|
|
89
|
+
*
|
|
90
|
+
* Only ever used to *seed* the age sidecar for a file that has none — the
|
|
91
|
+
* sidecar is authoritative from then on. See `LogFileWriter.creationTimeOf`
|
|
92
|
+
* for why the filesystem's answer cannot be trusted directly.
|
|
93
|
+
*/
|
|
94
|
+
fun creationTimeMillis(file: File): Long?
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The unit-test implementation. **Not for Android**: it is the only place in
|
|
98
|
+
* this package that touches `java.nio.file`, which does not exist below API
|
|
99
|
+
* 26.
|
|
100
|
+
*/
|
|
101
|
+
object Jvm : PlatformIo {
|
|
102
|
+
override fun linkCount(descriptor: FileDescriptor): Int = -1
|
|
103
|
+
|
|
104
|
+
override fun syncDirectory(directory: File): Boolean = directory.isDirectory
|
|
105
|
+
|
|
106
|
+
override fun lookup(file: File): Presence = Nio.lookup(file)
|
|
107
|
+
|
|
108
|
+
override fun isSymbolicLink(file: File): Boolean = Nio.isSymbolicLink(file)
|
|
109
|
+
|
|
110
|
+
override fun restrictToOwner(file: File, isDirectory: Boolean): Boolean {
|
|
111
|
+
// `false` as the second argument on the deny calls means "everyone",
|
|
112
|
+
// which is the point: strip group and other first, then grant the owner
|
|
113
|
+
// back. Doing it the other way round leaves a window where the file is
|
|
114
|
+
// readable by anyone.
|
|
115
|
+
var ok = file.setReadable(false, false)
|
|
116
|
+
ok = file.setWritable(false, false) && ok
|
|
117
|
+
ok = file.setExecutable(false, false) && ok
|
|
118
|
+
ok = file.setReadable(true, true) && ok
|
|
119
|
+
ok = file.setWritable(true, true) && ok
|
|
120
|
+
if (isDirectory) ok = file.setExecutable(true, true) && ok
|
|
121
|
+
return ok
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
override fun creationTimeMillis(file: File): Long? = Nio.creationTimeMillis(file)
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Every `java.nio.file` reference in the package, in one object that
|
|
128
|
+
* nothing on Android ever calls.
|
|
129
|
+
*
|
|
130
|
+
* Held separately from [Jvm] rather than inlined because
|
|
131
|
+
* [AndroidPlatformIo] *does* resolve [Jvm] — for its `restrictToOwner`
|
|
132
|
+
* fallback, which uses only `java.io` — and resolving it must not drag
|
|
133
|
+
* `java.nio.file` into an Android class load.
|
|
134
|
+
*/
|
|
135
|
+
private object Nio {
|
|
136
|
+
fun lookup(file: File): Presence = try {
|
|
137
|
+
val path = file.toPath()
|
|
138
|
+
val noFollow = arrayOf(java.nio.file.LinkOption.NOFOLLOW_LINKS)
|
|
139
|
+
when {
|
|
140
|
+
java.nio.file.Files.exists(path, *noFollow) -> Presence.PRESENT
|
|
141
|
+
java.nio.file.Files.notExists(path, *noFollow) -> Presence.ABSENT
|
|
142
|
+
else -> Presence.UNKNOWN
|
|
143
|
+
}
|
|
144
|
+
} catch (_: Throwable) {
|
|
145
|
+
Presence.UNKNOWN
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
fun isSymbolicLink(file: File): Boolean = try {
|
|
149
|
+
java.nio.file.Files.isSymbolicLink(file.toPath())
|
|
150
|
+
} catch (_: Throwable) {
|
|
151
|
+
true
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
fun creationTimeMillis(file: File): Long? = try {
|
|
155
|
+
val attributes = java.nio.file.Files.readAttributes(
|
|
156
|
+
file.toPath(),
|
|
157
|
+
java.nio.file.attribute.BasicFileAttributes::class.java
|
|
158
|
+
)
|
|
159
|
+
attributes.creationTime().toMillis().takeIf { it > 0 }
|
|
160
|
+
} catch (_: Throwable) {
|
|
161
|
+
null
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|