marsrs-react-native-xlog 0.1.0-alpha.3 → 0.1.0-alpha.4

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/ios/Xlog.swift CHANGED
@@ -1,4 +1,4 @@
1
- // The iOS half of `marsrs-react-native-xlog`: the eleven methods of the `Xlog`
1
+ // The iOS half of `marsrs-react-native-xlog`: the sixteen methods of the `Xlog`
2
2
  // native module, each of them a straight call of a `mars_xlog_*` symbol — the
3
3
  // C ABI of `crates/marsrs-ffi`, in the `marsrs-xlog.xcframework` the pod carries.
4
4
  //
@@ -50,8 +50,10 @@ internal final class Xlog: NSObject {
50
50
  ///
51
51
  /// `false` is a configuration it refused — an empty `logDir` or
52
52
  /// `namePrefix`, or a directory it cannot write to — and it is what the JS
53
- /// caller turns into a throw rather than a handle it would write through
54
- /// the process-wide appender with.
53
+ /// caller turns into a throw rather than a handle nothing was opened
54
+ /// for. `true` for a prefix that is already open says which appender the
55
+ /// prefix has, and not that the configuration was taken again: the C ABI
56
+ /// answers the appender that is already there.
55
57
  @objc(open:)
56
58
  internal func openAppender(_ config: [AnyHashable: Any]) -> Bool {
57
59
  let logDir = string(config, "logDir")
@@ -61,14 +63,14 @@ internal final class Xlog: NSObject {
61
63
  let namePrefix = string(config, "namePrefix").isEmpty
62
64
  ? Self.defaultNamePrefix
63
65
  : string(config, "namePrefix")
64
- // One appender per prefix: a prefix this module has already opened is
65
- // answered as it is, and not opened again over the first, which would be
66
- // a handle nothing releases. `src/index.ts` keeps the same map, so the
67
- // two names an app holds for one prefix are one appender, and `close` on
68
- // either is `close` on both.
69
- guard handles[namePrefix] == nil else {
70
- return true
71
- }
66
+ // The C ABI is what keeps one appender per prefix: it answers the
67
+ // handle of the appender this prefix already has rather than opening a
68
+ // second one over its files, so asking again is how a prefix another
69
+ // part of the app closed is opened again — and the handle this stores
70
+ // is the one the registry answers, and not a dead one the last `open`
71
+ // left behind. `src/index.ts` keeps the same map, so the two names an
72
+ // app holds for one prefix are one appender, and `close` on either is
73
+ // `close` on both.
72
74
  let level = int(config, "level", Int32(MarsLevelInfo.rawValue))
73
75
  let mode = int(config, "mode", Int32(MarsAppenderAsync.rawValue))
74
76
  let compressMode = int(config, "compressMode", Int32(MarsCompressZlib.rawValue))
@@ -102,67 +104,161 @@ internal final class Xlog: NSObject {
102
104
  /// empty: there is no JS frame to name, and the C++ writes an empty one too.
103
105
  @objc(log:level:tag:message:)
104
106
  internal func log(_ namePrefix: String, level: Double, tag: String, message: String) {
105
- guard let handle = handles[namePrefix] else {
107
+ guard let handle = openHandle(of: namePrefix), let native = int32(level) else {
106
108
  return
107
109
  }
108
110
  tag.withCString { cTag in
109
111
  message.withCString { cMessage in
110
- mars_xlog_write_instance(handle, Int32(level), cTag, nil, nil, 0, cMessage)
112
+ mars_xlog_write_instance(handle, native, cTag, nil, nil, 0, cMessage)
111
113
  }
112
114
  }
113
115
  }
114
116
 
117
+ /// `mars_xlog_current_log_path_instance`: the directory this appender
118
+ /// writes its files into, or `nil` once it is closed.
119
+ ///
120
+ /// A directory and not a file, because that is what the C++'s
121
+ /// `GetCurrentLogPath` answers; there is no "before the day's first record"
122
+ /// state, and the day's file is the question `logFiles` asks.
123
+ @objc(currentLogPath:)
124
+ internal func currentLogPath(of namePrefix: String) -> String? {
125
+ guard let handle = openHandle(of: namePrefix) else {
126
+ return nil
127
+ }
128
+ return path { out, len in
129
+ mars_xlog_current_log_path_instance(handle, out, len)
130
+ }
131
+ }
132
+
133
+ /// `mars_xlog_getfilepath_from_timespan_instance`: the log files of
134
+ /// `daysAgo` days ago that are *there* — what an app that uploads
135
+ /// yesterday's opens. `0` is today, `1` is yesterday, and so on.
136
+ @objc(logFiles:daysAgo:)
137
+ internal func logFiles(of namePrefix: String, daysAgo: Double) -> [String] {
138
+ dayPaths(of: namePrefix, daysAgo: daysAgo) { handle, timespan, index, out, len in
139
+ mars_xlog_getfilepath_from_timespan_instance(handle, timespan, index, out, len)
140
+ }
141
+ }
142
+
143
+ /// `mars_xlog_make_logfile_name_instance`: the paths of the log files of
144
+ /// `daysAgo` days ago, whether or not they are there yet.
145
+ @objc(logFileNames:daysAgo:)
146
+ internal func logFileNames(of namePrefix: String, daysAgo: Double) -> [String] {
147
+ dayPaths(of: namePrefix, daysAgo: daysAgo) { handle, timespan, index, out, len in
148
+ mars_xlog_make_logfile_name_instance(handle, timespan, index, out, len)
149
+ }
150
+ }
151
+
115
152
  /// `mars_xlog_is_enabled_for`: whether a record of the level would be
116
153
  /// written, which an app asks before it builds a message that is expensive
117
154
  /// to build.
118
155
  @objc(isLoggable:level:)
119
156
  internal func isLoggable(_ namePrefix: String, level: Double) -> Bool {
120
- guard let handle = handles[namePrefix] else {
157
+ guard let handle = openHandle(of: namePrefix), let native = int32(level) else {
121
158
  return false
122
159
  }
123
- return mars_xlog_is_enabled_for(handle, Int32(level)) != 0
160
+ return mars_xlog_is_enabled_for(handle, native) != 0
124
161
  }
125
162
 
126
163
  /// `mars_xlog_get_level`: what the appender answers, and not what JS holds.
164
+ ///
165
+ /// Asked of the handle [openHandle] answered, and not of the one [handles]
166
+ /// cached: a prefix is one appender to the C ABI, so an appender another
167
+ /// part of the app closed — a `MarsRSXlog.Xlog` of the same prefix — took
168
+ /// it out of the registry and left the handle cached here, and
169
+ /// `mars_xlog_get_level` answers `-1` for a handle it does not know.
127
170
  @objc(getLevel:)
128
171
  internal func level(of namePrefix: String) -> Double {
129
- guard let handle = handles[namePrefix] else {
172
+ guard let handle = openHandle(of: namePrefix) else {
130
173
  return Double(MARS_LEVEL_NONE)
131
174
  }
132
- return Double(mars_xlog_get_level(handle))
175
+ let native = mars_xlog_get_level(handle)
176
+ // A level `MarsLogLevel` does not carry — the `-1` above among them —
177
+ // is answered as `MARS_LEVEL_NONE` and not as it stands, which is the
178
+ // answer the Kotlin half of this module gives for the same numbers:
179
+ // `0`, `MarsLevelVerbose`, is the level that logs everything, so a
180
+ // level that is not one read as `0` is the opposite of what an
181
+ // appender that writes nothing was asked.
182
+ guard native >= 0, native <= MARS_LEVEL_NONE else {
183
+ return Double(MARS_LEVEL_NONE)
184
+ }
185
+ return Double(native)
133
186
  }
134
187
 
135
- /// `mars_xlog_flush_instance`.
136
- @objc(flush:sync:)
137
- internal func flush(_ namePrefix: String, sync: Bool) {
138
- guard let handle = handles[namePrefix] else {
188
+ /// `mars_xlog_request_flush_instance`: tells the writer thread it may take what is
189
+ /// in the cache to the log file, and returns at once — nothing waits, and
190
+ /// nothing is in the file because this returned. A record still in the cache
191
+ /// is in a file the kernel holds, so one whose process dies keeps it; what
192
+ /// an app wants before it reads the file or uploads it is `flushNow` or
193
+ /// `flush`.
194
+ @objc(requestFlush:)
195
+ internal func requestFlush(_ namePrefix: String) {
196
+ guard let handle = openHandle(of: namePrefix) else {
197
+ return
198
+ }
199
+ mars_xlog_request_flush_instance(handle)
200
+ }
201
+
202
+ /// `mars_xlog_flush_now_instance`: the drain is this thread's, so the records are
203
+ /// in the log file — handed to the OS, and not left in the file's own
204
+ /// `FILE*` — when it returns, and what it costs is the time the drain takes,
205
+ /// on the JS thread and nowhere else.
206
+ @objc(flushNow:)
207
+ internal func flushNow(_ namePrefix: String) {
208
+ guard let handle = openHandle(of: namePrefix) else {
209
+ return
210
+ }
211
+ mars_xlog_flush_now_instance(handle)
212
+ }
213
+
214
+ /// `flushNow` off the JS thread: the same drain, on a queue of this
215
+ /// module's own, and `resolve` called when it is over.
216
+ ///
217
+ /// The handle and not `self`: the closure runs on another thread.
218
+ /// `resolve` and `reject` are `RCTPromiseResolveBlock` and
219
+ /// `RCTPromiseRejectBlock`, spelled out because the bridging header shows
220
+ /// this file the C ABI and nothing else — a TurboModule method codegen
221
+ /// answers a `Promise` from is given the two blocks and resolves one of
222
+ /// them, and there is nothing here that can fail, so `reject` is never
223
+ /// called.
224
+ @objc(flush:resolve:reject:)
225
+ internal func flush(
226
+ _ namePrefix: String,
227
+ resolve: @escaping (Any?) -> Void,
228
+ reject: @escaping (String?, String?, Error?) -> Void
229
+ ) {
230
+ guard let handle = openHandle(of: namePrefix) else {
231
+ resolve(nil)
139
232
  return
140
233
  }
141
- mars_xlog_flush_instance(handle, sync ? 1 : 0)
234
+ Self.flushQueue.async {
235
+ mars_xlog_flush_now_instance(handle)
236
+ resolve(nil)
237
+ }
142
238
  }
143
239
 
144
240
  /// `mars_xlog_set_level_instance`.
145
241
  @objc(setLevel:level:)
146
242
  internal func setLevel(_ namePrefix: String, level: Double) {
147
- guard let handle = handles[namePrefix] else {
243
+ guard let handle = openHandle(of: namePrefix), let native = int32(level) else {
148
244
  return
149
245
  }
150
- mars_xlog_set_level_instance(handle, Int32(level))
246
+ mars_xlog_set_level_instance(handle, native)
151
247
  }
152
248
 
153
249
  /// `mars_xlog_set_mode_instance`.
154
250
  @objc(setMode:mode:)
155
251
  internal func setMode(_ namePrefix: String, mode: Double) {
156
- guard let handle = handles[namePrefix] else {
252
+ guard let handle = openHandle(of: namePrefix), let native = int32(mode) else {
157
253
  return
158
254
  }
159
- mars_xlog_set_mode_instance(handle, Int32(mode))
255
+ mars_xlog_set_mode_instance(handle, native)
160
256
  }
161
257
 
162
258
  /// `mars_xlog_set_console_log_instance`.
163
259
  @objc(setConsoleLogEnabled:enabled:)
164
260
  internal func setConsoleLogEnabled(_ namePrefix: String, enabled: Bool) {
165
- guard let handle = handles[namePrefix] else {
261
+ guard let handle = openHandle(of: namePrefix) else {
166
262
  return
167
263
  }
168
264
  mars_xlog_set_console_log_instance(handle, enabled ? 1 : 0)
@@ -173,36 +269,55 @@ internal final class Xlog: NSObject {
173
269
  /// 2^53.
174
270
  @objc(setMaxFileSize:bytes:)
175
271
  internal func setMaxFileSize(_ namePrefix: String, bytes: Double) {
176
- guard let handle = handles[namePrefix] else {
272
+ guard let handle = openHandle(of: namePrefix), let native = uint64(bytes) else {
177
273
  return
178
274
  }
179
- mars_xlog_set_max_file_size_instance(handle, UInt64(bytes))
275
+ mars_xlog_set_max_file_size_instance(handle, native)
180
276
  }
181
277
 
182
278
  /// `mars_xlog_set_max_alive_duration_instance`.
183
279
  @objc(setMaxAliveTime:seconds:)
184
280
  internal func setMaxAliveTime(_ namePrefix: String, seconds: Double) {
185
- guard let handle = handles[namePrefix] else {
281
+ guard let handle = openHandle(of: namePrefix), let native = int64(seconds) else {
186
282
  return
187
283
  }
188
- mars_xlog_set_max_alive_duration_instance(handle, Int64(seconds))
284
+ mars_xlog_set_max_alive_duration_instance(handle, native)
189
285
  }
190
286
 
191
- /// `mars_xlog_release_instance`: closes the appender `open` made.
287
+ /// `mars_xlog_release_instance_of`: closes the appender `open` made.
288
+ ///
289
+ /// The prefix is released only while the appender of it is still the one
290
+ /// `open` was answered with: a release is given a *prefix*, so one another
291
+ /// part of the app opened after this one was closed would be theirs and
292
+ /// would be closed by this. Naming the handle is what makes the question
293
+ /// and the release one call.
294
+ ///
295
+ /// Off the JS thread, the way `flush` is: a method codegen answers no
296
+ /// promise from is a call made on the JS thread, and a release is a drain
297
+ /// of everything the appender still holds and a write of the banner that
298
+ /// ends the file. The handle is taken out of `handles` here and not in the
299
+ /// block, so a reopen of the prefix that lands while the drain is queued
300
+ /// is a reopen of an appender this close never held.
192
301
  @objc(close:)
193
302
  internal func close(_ namePrefix: String) {
194
- guard handles.removeValue(forKey: namePrefix) != nil else {
303
+ guard let handle = handles.removeValue(forKey: namePrefix) else {
195
304
  return
196
305
  }
197
- namePrefix.withCString { mars_xlog_release_instance($0) }
306
+ Self.flushQueue.async {
307
+ namePrefix.withCString { prefix in
308
+ mars_xlog_release_instance_of(prefix, handle)
309
+ }
310
+ }
198
311
  }
199
312
 
200
313
  /// Closes what `open` opened and `close` was not asked about: the module
201
314
  /// goes away with the bridge, and an appender is a file its writer thread
202
315
  /// holds open.
203
316
  deinit {
204
- for namePrefix in handles.keys {
205
- namePrefix.withCString { mars_xlog_release_instance($0) }
317
+ for (namePrefix, handle) in handles {
318
+ namePrefix.withCString { prefix in
319
+ mars_xlog_release_instance_of(prefix, handle)
320
+ }
206
321
  }
207
322
  handles.removeAll()
208
323
  }
@@ -214,7 +329,12 @@ internal final class Xlog: NSObject {
214
329
  /// What the C++ passes on, which is the appender's own 6.
215
330
  private static let defaultCompressLevel: Int32 = 0
216
331
 
217
- /// `0` keeps every log file, which is what the C++'s default is.
332
+ /// The queue `flush` drains on, and why it is one queue and not the JS
333
+ /// thread: a drain blocks the thread it runs on, and serial is what keeps
334
+ /// two drains of one appender from being two writers in one file.
335
+ private static let flushQueue = DispatchQueue(label: "io.github.orangeboychen.marsrs.reactnative.xlog.flush")
336
+
337
+ /// `0` keeps every cache file, which is what the C++'s default is.
218
338
  private static let keepEveryFile: Int32 = 0
219
339
 
220
340
  /// The four fields of `MarsXLogConfig` that are C strings, in the order the
@@ -227,6 +347,27 @@ internal final class Xlog: NSObject {
227
347
  case cacheDir = 3
228
348
  }
229
349
 
350
+ /// The handle [handles] holds for `namePrefix`, and `nil` when it is not
351
+ /// the appender of that prefix any more: the registry is asked, and the
352
+ /// handle it answers is the one that is open.
353
+ ///
354
+ /// The registry and not the handle alone, which is the question the Swift
355
+ /// `Xlog` of the port asks in its own `openHandle`:
356
+ /// `mars_xlog_release_instance` releases the appender of a *prefix* and
357
+ /// not of a handle, so a handle whose appender something else closed is
358
+ /// still the number this dictionary holds, and every symbol of the C ABI
359
+ /// answers a handle it does not know — `mars_xlog_get_level` with `-1`,
360
+ /// which is a level of no `MarsLogLevel`.
361
+ private func openHandle(of namePrefix: String) -> Int64? {
362
+ guard let handle = handles[namePrefix] else {
363
+ return nil
364
+ }
365
+ let owns = namePrefix.withCString { prefix in
366
+ mars_xlog_get_instance(prefix) == handle
367
+ }
368
+ return owns ? handle : nil
369
+ }
370
+
230
371
  /// Runs `body` with every one of `strings` as a C string, and frees the
231
372
  /// copies it made: `MarsXLogConfig` holds pointers rather than copies, and a
232
373
  /// Swift `String` has no C string of its own to hand out — `withCString` is
@@ -250,4 +391,89 @@ internal final class Xlog: NSObject {
250
391
  private func int(_ config: [AnyHashable: Any], _ key: String, _ fallback: Int32) -> Int32 {
251
392
  (config[key] as? NSNumber)?.int32Value ?? fallback
252
393
  }
394
+
395
+ /// What the caller sent for a level or a mode, and `nil` when it is not one:
396
+ /// a `Double`, because JS has one number type, and `Int32(_:)` traps on a
397
+ /// `NaN`, an infinity and a number out of `Int32`'s range — a trap on the
398
+ /// JS thread is the app dying, where a level nobody asked for is a record
399
+ /// that is not written. What it answers for a number with a fraction in it
400
+ /// is the number without one, which is the same truncation the Kotlin half
401
+ /// of this module does.
402
+ private func int32(_ value: Double) -> Int32? {
403
+ guard value.isFinite, value >= Double(Int32.min), value <= Double(Int32.max) else {
404
+ return nil
405
+ }
406
+ return Int32(value)
407
+ }
408
+
409
+ /// What the caller sent for a file size, and `nil` when it is not one: a
410
+ /// `UInt64` a `NaN`, a negative number or one above `UInt64.max` is not,
411
+ /// and `UInt64(_:)` traps on all three.
412
+ private func uint64(_ value: Double) -> UInt64? {
413
+ guard value.isFinite, value >= 0, value < 18446744073709551616.0 else {
414
+ return nil
415
+ }
416
+ return UInt64(value)
417
+ }
418
+
419
+ /// What the caller sent for a file lifetime, and `nil` when it is not one:
420
+ /// the same trap in `Int64(_:)`, and `0` is the lifetime an appender opened
421
+ /// with none keeps — the C++'s own ten days, and not one a setter can ask
422
+ /// for, since a lifetime below a day is refused — so a lifetime of `NaN`
423
+ /// was one the app never asked for rather than one it was told had been
424
+ /// refused.
425
+ private func int64(_ value: Double) -> Int64? {
426
+ guard value.isFinite,
427
+ value >= -9223372036854775808.0, value < 9223372036854775808.0 else {
428
+ return nil
429
+ }
430
+ return Int64(value)
431
+ }
432
+ /// What `read` writes into the buffer it is handed, as a string; `nil` when
433
+ /// it wrote nothing — a negative code, or a path of no length.
434
+ ///
435
+ /// A negative code is `nil` whatever it is, `MARS_XLOG_ERR_NO_SPACE` among
436
+ /// them: a path that does not fit 1024 bytes ends the walk the way the end
437
+ /// of the list does. A symbol that answers a length rather than a pointer
438
+ /// is the C ABI's way of saying the caller decides how much it can hold.
439
+ private func path(of read: (UnsafeMutablePointer<CChar>, UInt32) -> Int32) -> String? {
440
+ var buffer = [CChar](repeating: 0, count: 1024)
441
+ let written = read(&buffer, UInt32(buffer.count))
442
+ guard written > 0 else {
443
+ return nil
444
+ }
445
+ return String(cString: buffer)
446
+ }
447
+
448
+ /// The paths of one day, walked index by index until the symbol answers that
449
+ /// there is nothing at that index: the list the C++ fills a `std::vector`
450
+ /// with, asked one at a time.
451
+ ///
452
+ /// `daysAgo` is a `Double` because JS has one number type, and it is read
453
+ /// with [`int32`] and not `Int32(_:)`: the latter traps on a value it
454
+ /// cannot represent — `NaN`, `Infinity`, a number out of `Int32`'s range
455
+ /// — and a trap on the JS thread is the app dying, where a day nobody
456
+ /// asked for is an empty list. [`int32`] truncates a fraction the way the
457
+ /// Kotlin half of this module does, so `1.5` days ago is yesterday on both
458
+ /// of them and not yesterday on one and nothing on the other.
459
+ private func dayPaths(
460
+ of namePrefix: String,
461
+ daysAgo: Double,
462
+ at symbol: (Int64, Int32, UInt32, UnsafeMutablePointer<CChar>, UInt32) -> Int32
463
+ ) -> [String] {
464
+ guard let handle = openHandle(of: namePrefix), let timespan = int32(daysAgo) else {
465
+ return []
466
+ }
467
+ var walked: [String] = []
468
+ var index: UInt32 = 0
469
+ while let found = path(of: { out, len in
470
+ symbol(handle, timespan, index, out, len)
471
+ }) {
472
+ walked.append(found)
473
+ index += 1
474
+ }
475
+ return walked
476
+ }
477
+
478
+
253
479
  }