@logbrew/react-native 0.1.10 → 0.1.11
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/README.md +68 -1
- package/android/src/main/java/co/logbrew/reactnative/EventRecordStore.java +611 -0
- package/android/src/main/java/co/logbrew/reactnative/FatalStoreModuleImpl.java +129 -0
- package/android/src/newarch/java/co/logbrew/reactnative/FatalStoreModule.java +26 -0
- package/android/src/oldarch/java/co/logbrew/reactnative/FatalStoreModule.java +26 -0
- package/index.cjs +2 -0
- package/index.d.cts +3 -0
- package/index.d.ts +3 -0
- package/index.js +2 -0
- package/index.native.d.ts +43 -1
- package/index.native.js +68 -1
- package/ios/LBRNEventRecordStore.h +29 -0
- package/ios/LBRNEventRecordStore.m +746 -0
- package/ios/LBRNFatalStoreModule.mm +159 -14
- package/ios/LBRNPrivateStorage.h +7 -0
- package/ios/LBRNPrivateStorage.m +38 -0
- package/package.json +7 -2
- package/persistent-delivery.native.js +282 -0
- package/src/NativeLogBrewFatalStore.ts +9 -0
package/README.md
CHANGED
|
@@ -118,6 +118,73 @@ client key before creating a new client. A `429` preserves the queue and
|
|
|
118
118
|
reports `pausedReason: "rate_limit"` plus the bounded retry signal exposed by
|
|
119
119
|
the failed flush.
|
|
120
120
|
|
|
121
|
+
## Offline And Restart Delivery
|
|
122
|
+
|
|
123
|
+
The React Native entry uses an app-private native queue by default when the
|
|
124
|
+
current app binary contains the linked LogBrew module. Each accepted event is
|
|
125
|
+
written before it enters memory. After a JavaScript runtime or app restart,
|
|
126
|
+
the client loads pending events oldest first with their original IDs. A
|
|
127
|
+
successful intake response commits which records were accepted before removing
|
|
128
|
+
them, so an interrupted removal can cause a duplicate but cannot silently lose
|
|
129
|
+
an unaccepted event. Replayed events keep their stable IDs. Delivery is at
|
|
130
|
+
least once, so apps must tolerate duplicates, including when retries regroup
|
|
131
|
+
events into different batches.
|
|
132
|
+
|
|
133
|
+
Inspect the delivery health snapshot to observe the active behavior:
|
|
134
|
+
|
|
135
|
+
```js
|
|
136
|
+
const health = client.deliveryHealth();
|
|
137
|
+
if (health.storage !== "persistent") {
|
|
138
|
+
// The app is running without the linked native queue.
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
The default `persistentQueue: "auto"` mode uses memory when the native module
|
|
143
|
+
is absent, including Expo Go and an older app binary after a JavaScript-only
|
|
144
|
+
update. For a production build that must not start without restart recovery,
|
|
145
|
+
set `persistentQueue: "required"`. Use `persistentQueue: "disabled"` only
|
|
146
|
+
when the app intentionally accepts a memory-only queue.
|
|
147
|
+
|
|
148
|
+
```js
|
|
149
|
+
const client = createLogBrewReactNativeClient({
|
|
150
|
+
clientKey,
|
|
151
|
+
persistentQueue: "required",
|
|
152
|
+
transport: createReactNativeFetchTransport()
|
|
153
|
+
});
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
The native queue stores one compact event per atomic record, is limited to
|
|
157
|
+
1,000 events and 4 MiB of compact event data, and uses the same smaller limits
|
|
158
|
+
when you configure them on the client. Both platforms use app-private storage.
|
|
159
|
+
The client key is not written to disk; its SHA-256 digest separates queues
|
|
160
|
+
after key rotation. The queue does not provide mathematically exactly-once
|
|
161
|
+
delivery.
|
|
162
|
+
|
|
163
|
+
A successful `shutdown()` drains and closes the queue. A failed flush or
|
|
164
|
+
shutdown leaves the exact remainder available to the same client and the next
|
|
165
|
+
app start. Call `client.purgePendingEvents()` only when no flush or shutdown is
|
|
166
|
+
active. To retire an active key without sending its remainder, purge that
|
|
167
|
+
client first and then close it:
|
|
168
|
+
|
|
169
|
+
```js
|
|
170
|
+
client.purgePendingEvents();
|
|
171
|
+
await client.shutdown();
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
When no client owns the key, remove any remaining records explicitly with:
|
|
175
|
+
|
|
176
|
+
```js
|
|
177
|
+
import { purgeLogBrewReactNativePersistentQueue } from "@logbrew/react-native";
|
|
178
|
+
|
|
179
|
+
purgeLogBrewReactNativePersistentQueue({ clientKey: previousClientKey });
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Use only one active persistent client for a given key across an app process.
|
|
183
|
+
The SDK rejects duplicates within one JavaScript runtime; apps with multiple
|
|
184
|
+
React Native runtimes or platform processes must coordinate that ownership.
|
|
185
|
+
Different client keys use separate native queues. The Node ESM and CommonJS
|
|
186
|
+
entries remain platform-neutral and never load React Native.
|
|
187
|
+
|
|
121
188
|
## Product Actions And API Milestones
|
|
122
189
|
|
|
123
190
|
Use explicit action and network helpers for important mobile funnel steps your app already understands. These events are designed for timelines and agent analysis without enabling broad automatic replay:
|
|
@@ -202,7 +269,7 @@ Installation is idempotent for the active React Native `ErrorUtils` object. The
|
|
|
202
269
|
|
|
203
270
|
The React Native conditional export obtains LogBrew's synchronous native fatal store through the supported TurboModule or `NativeModules` seam. Before chaining a fatal report, it writes one bounded record to app-private storage that is excluded from operating-system archives. On a later installation it performs stable-ID at-least-once replay, and acknowledgement happens only after local queue admission is observable through the SDK queue counters. Filtered, dropped, unknown-admission, persistence-failed, and acknowledgement-failed records are retained. A failed acknowledgement is retried without admitting the same ID twice in one JavaScript runtime. Use `fatalHealth()` for frozen bounded counters and status, or `discardPendingFatalRecord()` for an explicit rollback discard. The Node ESM and CommonJS entries never import React Native; non-React-Native callers must inject `fatalStore` explicitly.
|
|
204
271
|
|
|
205
|
-
Automatic events exclude the original error message, raw stack, arbitrary metadata, full URLs, hosts, query strings, local absolute paths, payloads, and native error text. `onDiagnostic` receives only a fixed code. This integration does not claim mathematically exactly-once delivery,
|
|
272
|
+
Automatic events exclude the original error message, raw stack, arbitrary metadata, full URLs, hosts, query strings, local absolute paths, payloads, and native error text. `onDiagnostic` receives only a fixed code. This error-handler integration does not claim mathematically exactly-once delivery, native crash capture, ANR or hang detection, general offline queueing by the fatal-record slot, or symbolication. The client-level persistent queue above owns normal event restart delivery.
|
|
206
273
|
|
|
207
274
|
### Opt-in Hermes Promise rejection tracking
|
|
208
275
|
|
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
package co.logbrew.reactnative;
|
|
2
|
+
|
|
3
|
+
import java.io.ByteArrayInputStream;
|
|
4
|
+
import java.io.ByteArrayOutputStream;
|
|
5
|
+
import java.io.DataInputStream;
|
|
6
|
+
import java.io.DataOutputStream;
|
|
7
|
+
import java.io.File;
|
|
8
|
+
import java.io.FileInputStream;
|
|
9
|
+
import java.io.FileOutputStream;
|
|
10
|
+
import java.io.IOException;
|
|
11
|
+
import java.nio.charset.StandardCharsets;
|
|
12
|
+
import java.util.ArrayList;
|
|
13
|
+
import java.util.Collections;
|
|
14
|
+
import java.util.Comparator;
|
|
15
|
+
import java.util.List;
|
|
16
|
+
import java.util.regex.Matcher;
|
|
17
|
+
import java.util.regex.Pattern;
|
|
18
|
+
import java.util.zip.CRC32;
|
|
19
|
+
|
|
20
|
+
/** Crash-safe app-private queue backing the synchronous React Native eventStore adapter. */
|
|
21
|
+
final class EventRecordStore {
|
|
22
|
+
static final String EVENT_PREFIX = "event-";
|
|
23
|
+
static final String EVENT_SUFFIX = ".record";
|
|
24
|
+
static final String MARKER_PREFIX = "accepted-";
|
|
25
|
+
static final String MARKER_SUFFIX = ".marker";
|
|
26
|
+
|
|
27
|
+
private static final int RECORD_MAGIC = 0x4c425145;
|
|
28
|
+
private static final int MARKER_MAGIC = 0x4c42514d;
|
|
29
|
+
private static final int FILE_FORMAT_VERSION = 1;
|
|
30
|
+
private static final int MAX_EVENT_BYTES = 4 * 1024 * 1024;
|
|
31
|
+
private static final int MAX_QUEUE_BYTES = 4 * 1024 * 1024;
|
|
32
|
+
private static final int MAX_QUEUE_RECORDS = 1000;
|
|
33
|
+
private static final Pattern EVENT_PATTERN =
|
|
34
|
+
Pattern.compile("^event-([0-9]{20})\\.record$");
|
|
35
|
+
private static final Pattern MARKER_PATTERN =
|
|
36
|
+
Pattern.compile("^accepted-([0-9]{20})\\.marker$");
|
|
37
|
+
private static final FatalRecordStore.ParentDirectorySync UNSUPPORTED_PARENT_DIRECTORY_SYNC =
|
|
38
|
+
directory -> FatalRecordStore.ParentDirectorySyncResult.UNSUPPORTED;
|
|
39
|
+
|
|
40
|
+
private final File directory;
|
|
41
|
+
private final FatalRecordStore.ParentDirectorySync parentDirectorySync;
|
|
42
|
+
private boolean poisoned;
|
|
43
|
+
|
|
44
|
+
EventRecordStore(File directory) {
|
|
45
|
+
this(directory, UNSUPPORTED_PARENT_DIRECTORY_SYNC);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
EventRecordStore(
|
|
49
|
+
File directory, FatalRecordStore.ParentDirectorySync parentDirectorySync) {
|
|
50
|
+
this.directory = resolveCanonicalParent(directory);
|
|
51
|
+
this.parentDirectorySync = parentDirectorySync;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
synchronized Result load() {
|
|
55
|
+
Snapshot snapshot = snapshot();
|
|
56
|
+
return snapshot == null
|
|
57
|
+
? Result.status("storage_error")
|
|
58
|
+
: new Result("loaded", snapshot.records);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
synchronized Result append(String serializedEvent, int eventBytes) {
|
|
62
|
+
if (poisoned || !validEvent(serializedEvent, eventBytes)) {
|
|
63
|
+
return Result.status("storage_error");
|
|
64
|
+
}
|
|
65
|
+
Snapshot snapshot = snapshot();
|
|
66
|
+
if (snapshot == null
|
|
67
|
+
|| snapshot.records.size() >= MAX_QUEUE_RECORDS
|
|
68
|
+
|| snapshot.totalEventBytes + (long) eventBytes > MAX_QUEUE_BYTES) {
|
|
69
|
+
return Result.status("storage_error");
|
|
70
|
+
}
|
|
71
|
+
long sequence = Math.max(snapshot.markerSequence, snapshot.maximumRecordSequence);
|
|
72
|
+
if (sequence == Long.MAX_VALUE) {
|
|
73
|
+
return Result.status("storage_error");
|
|
74
|
+
}
|
|
75
|
+
sequence += 1;
|
|
76
|
+
File recordFile = new File(directory, eventFileName(sequence));
|
|
77
|
+
if (!atomicWrite(recordFile, encodeRecord(sequence, serializedEvent, eventBytes))) {
|
|
78
|
+
return Result.status("storage_error");
|
|
79
|
+
}
|
|
80
|
+
return Result.status("appended");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
synchronized Result acknowledge(int count) {
|
|
84
|
+
if (poisoned || count < 0) {
|
|
85
|
+
return Result.status("storage_error");
|
|
86
|
+
}
|
|
87
|
+
Snapshot snapshot = snapshot();
|
|
88
|
+
if (snapshot == null || count > snapshot.records.size()) {
|
|
89
|
+
return Result.status("storage_error");
|
|
90
|
+
}
|
|
91
|
+
if (count == 0) {
|
|
92
|
+
return Result.status("acknowledged");
|
|
93
|
+
}
|
|
94
|
+
long acceptedSequence = snapshot.records.get(count - 1).sequence;
|
|
95
|
+
if (!commitMarker(acceptedSequence)) {
|
|
96
|
+
return Result.status("storage_error");
|
|
97
|
+
}
|
|
98
|
+
removeAcceptedFiles(acceptedSequence);
|
|
99
|
+
return Result.status("acknowledged");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
synchronized Result purge() {
|
|
103
|
+
if (poisoned || !prepareDirectory() || !removeStaleTemporaryFiles()) {
|
|
104
|
+
return Result.status("storage_error");
|
|
105
|
+
}
|
|
106
|
+
File[] files = directory.listFiles();
|
|
107
|
+
if (files == null) {
|
|
108
|
+
return Result.status("storage_error");
|
|
109
|
+
}
|
|
110
|
+
long maximumSequence = 0;
|
|
111
|
+
for (File file : files) {
|
|
112
|
+
Matcher eventMatcher = EVENT_PATTERN.matcher(file.getName());
|
|
113
|
+
Matcher markerMatcher = MARKER_PATTERN.matcher(file.getName());
|
|
114
|
+
Matcher matcher = eventMatcher.matches() ? eventMatcher : markerMatcher.matches() ? markerMatcher : null;
|
|
115
|
+
if (matcher == null || !isSafeRegularFile(file)) {
|
|
116
|
+
return Result.status("storage_error");
|
|
117
|
+
}
|
|
118
|
+
Long sequence = parseSequence(matcher.group(1));
|
|
119
|
+
if (sequence == null) {
|
|
120
|
+
return Result.status("storage_error");
|
|
121
|
+
}
|
|
122
|
+
maximumSequence = Math.max(maximumSequence, sequence);
|
|
123
|
+
}
|
|
124
|
+
if (maximumSequence == 0) {
|
|
125
|
+
return Result.status("purged");
|
|
126
|
+
}
|
|
127
|
+
if (maximumSequence == Long.MAX_VALUE) {
|
|
128
|
+
return Result.status("storage_error");
|
|
129
|
+
}
|
|
130
|
+
long acceptedSequence = maximumSequence + 1;
|
|
131
|
+
if (!commitMarker(acceptedSequence)) {
|
|
132
|
+
return Result.status("storage_error");
|
|
133
|
+
}
|
|
134
|
+
removeAcceptedFiles(acceptedSequence);
|
|
135
|
+
removeAllOlderMarkers(acceptedSequence);
|
|
136
|
+
return Result.status("purged");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
synchronized Result close() {
|
|
140
|
+
return Result.status(poisoned ? "storage_error" : "closed");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private Snapshot snapshot() {
|
|
144
|
+
if (poisoned || !prepareDirectory() || !removeStaleTemporaryFiles()) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
File[] files = directory.listFiles();
|
|
148
|
+
if (files == null) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
long markerSequence = 0;
|
|
153
|
+
List<FileSequence> eventFiles = new ArrayList<>();
|
|
154
|
+
List<FileSequence> markerFiles = new ArrayList<>();
|
|
155
|
+
for (File file : files) {
|
|
156
|
+
String name = file.getName();
|
|
157
|
+
Matcher eventMatcher = EVENT_PATTERN.matcher(name);
|
|
158
|
+
Matcher markerMatcher = MARKER_PATTERN.matcher(name);
|
|
159
|
+
if (eventMatcher.matches()) {
|
|
160
|
+
Long sequence = parseSequence(eventMatcher.group(1));
|
|
161
|
+
if (sequence == null || !isSafeRegularFile(file)) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
eventFiles.add(new FileSequence(file, sequence));
|
|
165
|
+
} else if (markerMatcher.matches()) {
|
|
166
|
+
Long sequence = parseSequence(markerMatcher.group(1));
|
|
167
|
+
if (sequence == null || !isSafeRegularFile(file)) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
markerSequence = Math.max(markerSequence, sequence);
|
|
171
|
+
markerFiles.add(new FileSequence(file, sequence));
|
|
172
|
+
} else {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (markerSequence > 0) {
|
|
177
|
+
File newestMarker = new File(directory, markerFileName(markerSequence));
|
|
178
|
+
if (!isSafeRegularFile(newestMarker) || !validMarker(newestMarker, markerSequence)) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
eventFiles.sort(Comparator.comparingLong(value -> value.sequence));
|
|
184
|
+
List<Record> records = new ArrayList<>();
|
|
185
|
+
long totalEventBytes = 0;
|
|
186
|
+
long maximumRecordSequence = 0;
|
|
187
|
+
for (FileSequence value : eventFiles) {
|
|
188
|
+
maximumRecordSequence = Math.max(maximumRecordSequence, value.sequence);
|
|
189
|
+
if (value.sequence <= markerSequence) {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
Record record = decodeRecord(value.file, value.sequence);
|
|
193
|
+
if (record == null) {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
totalEventBytes += record.eventBytes;
|
|
197
|
+
if (records.size() >= MAX_QUEUE_RECORDS || totalEventBytes > MAX_QUEUE_BYTES) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
records.add(record);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
removeAcceptedFiles(markerSequence);
|
|
204
|
+
removeOldMarkers(markerFiles, markerSequence);
|
|
205
|
+
return new Snapshot(
|
|
206
|
+
markerSequence, maximumRecordSequence, totalEventBytes, records);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private boolean commitMarker(long sequence) {
|
|
210
|
+
File marker = new File(directory, markerFileName(sequence));
|
|
211
|
+
if (marker.exists()) {
|
|
212
|
+
return isSafeRegularFile(marker) && validMarker(marker, sequence);
|
|
213
|
+
}
|
|
214
|
+
return atomicWrite(marker, encodeMarker(sequence));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private void removeAcceptedFiles(long markerSequence) {
|
|
218
|
+
if (markerSequence <= 0) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
File[] files = directory.listFiles();
|
|
222
|
+
if (files == null) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
boolean deleted = false;
|
|
226
|
+
for (File file : files) {
|
|
227
|
+
Matcher matcher = EVENT_PATTERN.matcher(file.getName());
|
|
228
|
+
if (!matcher.matches()) {
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
Long sequence = parseSequence(matcher.group(1));
|
|
232
|
+
if (sequence != null
|
|
233
|
+
&& sequence <= markerSequence
|
|
234
|
+
&& isSafeRegularFile(file)
|
|
235
|
+
&& file.delete()) {
|
|
236
|
+
deleted = true;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (deleted) {
|
|
240
|
+
parentDirectorySync.sync(directory);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private void removeOldMarkers(List<FileSequence> markers, long markerSequence) {
|
|
245
|
+
boolean deleted = false;
|
|
246
|
+
for (FileSequence marker : markers) {
|
|
247
|
+
if (marker.sequence < markerSequence
|
|
248
|
+
&& isSafeRegularFile(marker.file)
|
|
249
|
+
&& marker.file.delete()) {
|
|
250
|
+
deleted = true;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (deleted) {
|
|
254
|
+
parentDirectorySync.sync(directory);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private void removeAllOlderMarkers(long markerSequence) {
|
|
259
|
+
File[] files = directory.listFiles();
|
|
260
|
+
if (files == null) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
List<FileSequence> markers = new ArrayList<>();
|
|
264
|
+
for (File file : files) {
|
|
265
|
+
Matcher matcher = MARKER_PATTERN.matcher(file.getName());
|
|
266
|
+
if (!matcher.matches()) {
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
Long sequence = parseSequence(matcher.group(1));
|
|
270
|
+
if (sequence != null) {
|
|
271
|
+
markers.add(new FileSequence(file, sequence));
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
removeOldMarkers(markers, markerSequence);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private boolean prepareDirectory() {
|
|
278
|
+
try {
|
|
279
|
+
if (directory.exists()) {
|
|
280
|
+
if (!directory.isDirectory() || !isCanonicalPath(directory)) {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
} else if (!directory.mkdirs()) {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
return makeDirectoryPrivate(directory) && isCanonicalPath(directory);
|
|
287
|
+
} catch (IOException | SecurityException error) {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private boolean removeStaleTemporaryFiles() {
|
|
293
|
+
File[] files = directory.listFiles((unused, name) -> name.endsWith(".tmp"));
|
|
294
|
+
if (files == null) {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
for (File file : files) {
|
|
298
|
+
if (!isSafeRegularFile(file) || !file.delete()) {
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private boolean atomicWrite(File destination, byte[] bytes) {
|
|
306
|
+
if (bytes == null || bytes.length == 0 || destination.exists()) {
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
File temporary = new File(directory, destination.getName() + ".tmp");
|
|
310
|
+
if (temporary.exists()
|
|
311
|
+
&& (!isSafeRegularFile(temporary) || !temporary.delete())) {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
boolean committed = false;
|
|
315
|
+
boolean renamed = false;
|
|
316
|
+
try {
|
|
317
|
+
if (!temporary.createNewFile()
|
|
318
|
+
|| !isSafeRegularFile(temporary)
|
|
319
|
+
|| !makeFilePrivate(temporary)) {
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
try (FileOutputStream output = new FileOutputStream(temporary, false)) {
|
|
323
|
+
output.write(bytes);
|
|
324
|
+
output.flush();
|
|
325
|
+
output.getFD().sync();
|
|
326
|
+
}
|
|
327
|
+
if (!makeFilePrivate(temporary) || !temporary.renameTo(destination)) {
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
renamed = true;
|
|
331
|
+
if (!makeFilePrivate(destination)) {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
if (parentDirectorySync.sync(directory)
|
|
335
|
+
== FatalRecordStore.ParentDirectorySyncResult.FAILED) {
|
|
336
|
+
poisoned = true;
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
committed = true;
|
|
340
|
+
return true;
|
|
341
|
+
} catch (IOException | SecurityException error) {
|
|
342
|
+
return false;
|
|
343
|
+
} finally {
|
|
344
|
+
if (!committed) {
|
|
345
|
+
if (renamed) {
|
|
346
|
+
poisoned = true;
|
|
347
|
+
} else if (temporary.exists() && isSafeRegularFile(temporary)) {
|
|
348
|
+
temporary.delete();
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
private static boolean validEvent(String serializedEvent, int eventBytes) {
|
|
355
|
+
if (serializedEvent == null || serializedEvent.isEmpty() || eventBytes <= 0) {
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
byte[] bytes = serializedEvent.getBytes(StandardCharsets.UTF_8);
|
|
359
|
+
return bytes.length == eventBytes && bytes.length <= MAX_EVENT_BYTES;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
private static byte[] encodeRecord(
|
|
363
|
+
long sequence, String serializedEvent, int eventBytes) {
|
|
364
|
+
byte[] payload = serializedEvent.getBytes(StandardCharsets.UTF_8);
|
|
365
|
+
CRC32 checksum = new CRC32();
|
|
366
|
+
checksum.update(payload);
|
|
367
|
+
try {
|
|
368
|
+
ByteArrayOutputStream bytes = new ByteArrayOutputStream(payload.length + 32);
|
|
369
|
+
try (DataOutputStream output = new DataOutputStream(bytes)) {
|
|
370
|
+
output.writeInt(RECORD_MAGIC);
|
|
371
|
+
output.writeInt(FILE_FORMAT_VERSION);
|
|
372
|
+
output.writeLong(sequence);
|
|
373
|
+
output.writeInt(eventBytes);
|
|
374
|
+
output.writeInt(payload.length);
|
|
375
|
+
output.write(payload);
|
|
376
|
+
output.writeLong(checksum.getValue());
|
|
377
|
+
}
|
|
378
|
+
return bytes.toByteArray();
|
|
379
|
+
} catch (IOException impossible) {
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
private static Record decodeRecord(File file, long expectedSequence) {
|
|
385
|
+
if (file.length() <= 0 || file.length() > MAX_EVENT_BYTES + 32L) {
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
byte[] bytes = readBounded(file, MAX_EVENT_BYTES + 32);
|
|
389
|
+
if (bytes == null) {
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
try (DataInputStream input = new DataInputStream(new ByteArrayInputStream(bytes))) {
|
|
393
|
+
if (input.readInt() != RECORD_MAGIC || input.readInt() != FILE_FORMAT_VERSION) {
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
long sequence = input.readLong();
|
|
397
|
+
int eventBytes = input.readInt();
|
|
398
|
+
int payloadLength = input.readInt();
|
|
399
|
+
if (sequence != expectedSequence
|
|
400
|
+
|| eventBytes <= 0
|
|
401
|
+
|| payloadLength != eventBytes
|
|
402
|
+
|| payloadLength > MAX_EVENT_BYTES) {
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
byte[] payload = new byte[payloadLength];
|
|
406
|
+
input.readFully(payload);
|
|
407
|
+
long expectedChecksum = input.readLong();
|
|
408
|
+
if (input.read() != -1) {
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
CRC32 checksum = new CRC32();
|
|
412
|
+
checksum.update(payload);
|
|
413
|
+
if (checksum.getValue() != expectedChecksum) {
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
String serializedEvent = new String(payload, StandardCharsets.UTF_8);
|
|
417
|
+
if (!validEvent(serializedEvent, eventBytes)) {
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
return new Record(sequence, serializedEvent, eventBytes);
|
|
421
|
+
} catch (IOException error) {
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
private static byte[] encodeMarker(long sequence) {
|
|
427
|
+
CRC32 checksum = new CRC32();
|
|
428
|
+
checksum.update(longBytes(sequence));
|
|
429
|
+
try {
|
|
430
|
+
ByteArrayOutputStream bytes = new ByteArrayOutputStream(24);
|
|
431
|
+
try (DataOutputStream output = new DataOutputStream(bytes)) {
|
|
432
|
+
output.writeInt(MARKER_MAGIC);
|
|
433
|
+
output.writeInt(FILE_FORMAT_VERSION);
|
|
434
|
+
output.writeLong(sequence);
|
|
435
|
+
output.writeLong(checksum.getValue());
|
|
436
|
+
}
|
|
437
|
+
return bytes.toByteArray();
|
|
438
|
+
} catch (IOException impossible) {
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
private static boolean validMarker(File file, long expectedSequence) {
|
|
444
|
+
byte[] bytes = readBounded(file, 24);
|
|
445
|
+
if (bytes == null || bytes.length != 24) {
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
try (DataInputStream input = new DataInputStream(new ByteArrayInputStream(bytes))) {
|
|
449
|
+
if (input.readInt() != MARKER_MAGIC || input.readInt() != FILE_FORMAT_VERSION) {
|
|
450
|
+
return false;
|
|
451
|
+
}
|
|
452
|
+
long sequence = input.readLong();
|
|
453
|
+
long expectedChecksum = input.readLong();
|
|
454
|
+
CRC32 checksum = new CRC32();
|
|
455
|
+
checksum.update(longBytes(sequence));
|
|
456
|
+
return sequence == expectedSequence
|
|
457
|
+
&& sequence > 0
|
|
458
|
+
&& checksum.getValue() == expectedChecksum
|
|
459
|
+
&& input.read() == -1;
|
|
460
|
+
} catch (IOException error) {
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
private static byte[] longBytes(long value) {
|
|
466
|
+
return new byte[] {
|
|
467
|
+
(byte) (value >>> 56),
|
|
468
|
+
(byte) (value >>> 48),
|
|
469
|
+
(byte) (value >>> 40),
|
|
470
|
+
(byte) (value >>> 32),
|
|
471
|
+
(byte) (value >>> 24),
|
|
472
|
+
(byte) (value >>> 16),
|
|
473
|
+
(byte) (value >>> 8),
|
|
474
|
+
(byte) value
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
private static byte[] readBounded(File file, int maximumBytes) {
|
|
479
|
+
try (FileInputStream input = new FileInputStream(file);
|
|
480
|
+
ByteArrayOutputStream output = new ByteArrayOutputStream()) {
|
|
481
|
+
byte[] buffer = new byte[8192];
|
|
482
|
+
int total = 0;
|
|
483
|
+
int count;
|
|
484
|
+
while ((count = input.read(buffer)) != -1) {
|
|
485
|
+
total += count;
|
|
486
|
+
if (total > maximumBytes) {
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
output.write(buffer, 0, count);
|
|
490
|
+
}
|
|
491
|
+
return output.toByteArray();
|
|
492
|
+
} catch (IOException error) {
|
|
493
|
+
return null;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
private static String eventFileName(long sequence) {
|
|
498
|
+
return String.format(java.util.Locale.ROOT, "%s%020d%s", EVENT_PREFIX, sequence, EVENT_SUFFIX);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
private static String markerFileName(long sequence) {
|
|
502
|
+
return String.format(
|
|
503
|
+
java.util.Locale.ROOT, "%s%020d%s", MARKER_PREFIX, sequence, MARKER_SUFFIX);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
private static Long parseSequence(String value) {
|
|
507
|
+
try {
|
|
508
|
+
long sequence = Long.parseLong(value);
|
|
509
|
+
return sequence > 0 ? sequence : null;
|
|
510
|
+
} catch (NumberFormatException error) {
|
|
511
|
+
return null;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
private static File resolveCanonicalParent(File value) {
|
|
516
|
+
File absolute = value.getAbsoluteFile();
|
|
517
|
+
File parent = absolute.getParentFile();
|
|
518
|
+
if (parent == null) {
|
|
519
|
+
return absolute;
|
|
520
|
+
}
|
|
521
|
+
try {
|
|
522
|
+
return new File(parent.getCanonicalFile(), absolute.getName());
|
|
523
|
+
} catch (IOException | SecurityException error) {
|
|
524
|
+
return absolute;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
private static boolean isCanonicalPath(File value) throws IOException {
|
|
529
|
+
return value.getCanonicalFile().equals(value.getAbsoluteFile());
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
private static boolean isSafeRegularFile(File value) {
|
|
533
|
+
try {
|
|
534
|
+
return value.isFile()
|
|
535
|
+
&& isCanonicalPath(value);
|
|
536
|
+
} catch (IOException | SecurityException error) {
|
|
537
|
+
return false;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
private static boolean makeDirectoryPrivate(File value) {
|
|
542
|
+
return value.setReadable(false, false)
|
|
543
|
+
&& value.setWritable(false, false)
|
|
544
|
+
&& value.setExecutable(false, false)
|
|
545
|
+
&& value.setReadable(true, true)
|
|
546
|
+
&& value.setWritable(true, true)
|
|
547
|
+
&& value.setExecutable(true, true);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
private static boolean makeFilePrivate(File value) {
|
|
551
|
+
return value.setReadable(false, false)
|
|
552
|
+
&& value.setWritable(false, false)
|
|
553
|
+
&& value.setExecutable(false, false)
|
|
554
|
+
&& value.setReadable(true, true)
|
|
555
|
+
&& value.setWritable(true, true);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
static final class Record {
|
|
559
|
+
final long sequence;
|
|
560
|
+
final String serializedEvent;
|
|
561
|
+
final int eventBytes;
|
|
562
|
+
|
|
563
|
+
Record(long sequence, String serializedEvent, int eventBytes) {
|
|
564
|
+
this.sequence = sequence;
|
|
565
|
+
this.serializedEvent = serializedEvent;
|
|
566
|
+
this.eventBytes = eventBytes;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
static final class Result {
|
|
571
|
+
final String status;
|
|
572
|
+
final List<Record> records;
|
|
573
|
+
|
|
574
|
+
Result(String status, List<Record> records) {
|
|
575
|
+
this.status = status;
|
|
576
|
+
this.records = Collections.unmodifiableList(new ArrayList<>(records));
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
static Result status(String status) {
|
|
580
|
+
return new Result(status, Collections.emptyList());
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
private static final class Snapshot {
|
|
585
|
+
final long markerSequence;
|
|
586
|
+
final long maximumRecordSequence;
|
|
587
|
+
final long totalEventBytes;
|
|
588
|
+
final List<Record> records;
|
|
589
|
+
|
|
590
|
+
Snapshot(
|
|
591
|
+
long markerSequence,
|
|
592
|
+
long maximumRecordSequence,
|
|
593
|
+
long totalEventBytes,
|
|
594
|
+
List<Record> records) {
|
|
595
|
+
this.markerSequence = markerSequence;
|
|
596
|
+
this.maximumRecordSequence = maximumRecordSequence;
|
|
597
|
+
this.totalEventBytes = totalEventBytes;
|
|
598
|
+
this.records = records;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
private static final class FileSequence {
|
|
603
|
+
final File file;
|
|
604
|
+
final long sequence;
|
|
605
|
+
|
|
606
|
+
FileSequence(File file, long sequence) {
|
|
607
|
+
this.file = file;
|
|
608
|
+
this.sequence = sequence;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|