JavaScriptWorker.swift 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. import Foundation
  2. import JavaScriptCore
  3. private let contextLock = NSRecursiveLock()
  4. final class JavaScriptWorker {
  5. private let processQueue = DispatchQueue(label: "DispatchQueue.JavaScriptWorker")
  6. private let virtualMachine: JSVirtualMachine
  7. @SyncAccess(lock: contextLock) private var commonContext: JSContext? = nil
  8. private var aggregatedLogs: [String] = [] // Step 1: Property to store log messages
  9. init() {
  10. virtualMachine = processQueue.sync { JSVirtualMachine()! }
  11. }
  12. private func createContext() -> JSContext {
  13. let context = JSContext(virtualMachine: virtualMachine)!
  14. context.exceptionHandler = { _, exception in
  15. if let error = exception?.toString() {
  16. warning(.openAPS, "JavaScript Error: \(error)")
  17. }
  18. }
  19. let consoleLog: @convention(block) (String) -> Void = { message in
  20. let trimmedMessage = message.trimmingCharacters(in: .whitespacesAndNewlines)
  21. if !trimmedMessage.isEmpty {
  22. self.aggregatedLogs.append(trimmedMessage)
  23. }
  24. }
  25. context.setObject(
  26. consoleLog,
  27. forKeyedSubscript: "_consoleLog" as NSString
  28. )
  29. return context
  30. }
  31. // New method to flush aggregated logs
  32. private func aggregateLogs() {
  33. let patternsAndReplacements: [(pattern: String, replacement: String)] = [
  34. (
  35. "Middleware reason: (.*)",
  36. "\"middlewareReason\": \"$1\", "
  37. ),
  38. (
  39. "Pumphistory is empty!",
  40. "\"pumpHistory\": \"empty\", "
  41. ),
  42. (
  43. "insulinFactor set to : (-?\\d+(\\.\\d+)?)",
  44. "\"insulineFactor\": \"$1\", "
  45. ),
  46. (
  47. "Using weighted TDD average: (-?\\d+(\\.\\d+)?) U",
  48. "\"weightedTDDAverage\": \"$1\", "
  49. ),
  50. (
  51. ", instead of past 24 h \\((-?\\d+(\\.\\d+)?) U\\)",
  52. "\"past24TTDAverage\": \"$1\", "
  53. ),
  54. (
  55. ", weight: (-?\\d+(\\.\\d+)?)",
  56. "\"weight\": \"$1\", "
  57. ),
  58. (
  59. ", Dynamic ratios log: (.*)",
  60. "\"dynamicRatiosLog\": \"$1\", "
  61. ),
  62. (
  63. "Default Half Basal Target used: (-?\\d+(\\.\\d+)?) mmol/L",
  64. "\"halfBasalTarget\": \"$1\", "
  65. ),
  66. (
  67. "Autosens ratio: (-?\\d+(\\.\\d+)?);",
  68. "\"autosensRatio\": \"$1\", "
  69. ),
  70. (
  71. "Threshold set to (-?\\d+(\\.\\d+)?)",
  72. "\"threshold\": \"$1\", "
  73. ),
  74. (
  75. "ISF unchanged: (-?\\d+(\\.\\d+)?)",
  76. "\"isf\": \"$1\", " + "\"prevIsf\": \"$1\", "
  77. ),
  78. (
  79. "ISF from (-?\\d+(\\.\\d+)?) to (-?\\d+(\\.\\d+)?)",
  80. "\"isf\": \"$3\", " + "\"prevIsf\": \"$1\", "
  81. ),
  82. (
  83. "CR:(-?\\d+(\\.\\d+)?)",
  84. "\"cr\": \"$1\", "
  85. ),
  86. (
  87. "currenttemp:(-?\\d+(\\.\\d+)?) lastTempAge:(-?\\d+(\\.\\d+)?)m, tempModulus:(-?\\d+(\\.\\d+)?)m",
  88. "\"currenttemp\": \"$1\", " + "\"lastTempAge\": \"$3\", " + "\"tempModulus\": \"$5\", "
  89. ),
  90. (
  91. "SMB (\\w+) \\((.*)\\)",
  92. "\"smb\": \"$1\", " + "\"smbReason\": \"$2\", "
  93. ),
  94. (
  95. "profile.sens:(-?\\d+(\\.\\d+)?), sens:(-?\\d+(\\.\\d+)?), CSF:(-?\\d+(\\.\\d+)?)",
  96. "\"profileSens\": \"$1\", " + "\"sens\": \"$3\", " + "\"csf\": \"$5\", "
  97. ),
  98. (
  99. "Carb Impact:(-?\\d+(\\.\\d+)?)mg/dL per 5m; CI Duration:(-?\\d+(\\.\\d+)?)hours; remaining CI \\((-?\\d+(\\.\\d+)?)h peak\\):(-?\\d+(\\.\\d+)?)mg/dL per 5m",
  100. "\"carbImpact\": \"$1\", " + "\"carbImpactDuration\": \"$3\", " + "\"carbImpactRemainingTime\": \"$5\", " +
  101. "\"carbImpactRemaining\": \"$7\", "
  102. ),
  103. (
  104. "UAM Impact:(-?\\d+(\\.\\d+)?)mg/dL per 5m; UAM Duration:(-?\\d+(\\.\\d+)?)hours",
  105. "\"uamImpact\": \"$1\", " + "\"uamImpactDuration\": \"$3\", "
  106. ),
  107. (
  108. "minPredBG: (-?\\d+(\\.\\d+)?) minIOBPredBG: (-?\\d+(\\.\\d+)?) minZTGuardBG: (-?\\d+(\\.\\d+)?)",
  109. "\"minPredBG\": \"$1\", " + "\"minIOBPredBG\": \"$3\", " + "\"minZTGuardBG\": \"$5\", "
  110. ),
  111. (
  112. "avgPredBG:(-?\\d+(\\.\\d+)?) COB\\/Carbs:(-?\\d+(\\.\\d+)?)\\/(-?\\d+(\\.\\d+)?)",
  113. "\"avgPredBG\": \"$1\", " + "\"cob\": \"$3\", " + "\"carbs\": \"$5\", "
  114. ),
  115. (
  116. "BG projected to remain above (-?\\d+(\\.\\d+)?) for (-?\\d+(\\.\\d+)?)minutes",
  117. "\"projectedBG\": \"$1\", " + "\"projectedBGDuration\": \"$3\", "
  118. ),
  119. (
  120. "naive_eventualBG:,(-?\\d+(\\.\\d+)?),bgUndershoot:,(-?\\d+(\\.\\d+)?),zeroTempDuration:,(-?\\d+(\\.\\d+)?),zeroTempEffect:,(-?\\d+(\\.\\d+)?),carbsReq:,(-?\\d+(\\.\\d+)?)",
  121. "\"naiveEventualBG\": \"$1\", " + "\"bgUndershoot\": \"$3\", " + "\"zeroTempDuration\": \"$5\", " +
  122. "\"zeroTempEffect\": \"$7\", " + "\"carbsReq\": \"$9\", "
  123. ),
  124. (
  125. "(.*) \\(\\.? insulinReq: (-?\\d+(\\.\\d+)?) U\\)",
  126. "\"insulinReqReason\": \"$1\", " + "\"insulinReq\": \"$2\", "
  127. ),
  128. (
  129. "(.*) \\(\\.? insulinForManualBolus: (-?\\d+(\\.\\d+)?) U\\)",
  130. "\"insulinForManualBolusReason\": \"$1\", " + "\"insulinForManualBolus\": \"$2\", "
  131. ),
  132. (
  133. "Setting neutral temp basal of (-?\\d+(\\.\\d+)?)U/hr",
  134. "\"basalRate\": \"$1\"/hr', "
  135. )
  136. ]
  137. var combinedLogs = aggregatedLogs.joined(separator: "\n").trimmingCharacters(in: .whitespacesAndNewlines)
  138. aggregatedLogs.removeAll()
  139. if !combinedLogs.isEmpty {
  140. // Apply each pattern and replace matches
  141. for (pattern, replacement) in patternsAndReplacements {
  142. if let regex = try? NSRegularExpression(pattern: pattern, options: []) {
  143. let range = NSRange(combinedLogs.startIndex..., in: combinedLogs)
  144. combinedLogs = regex.stringByReplacingMatches(
  145. in: combinedLogs,
  146. options: [],
  147. range: range,
  148. withTemplate: replacement
  149. )
  150. } else {
  151. error(.openAPS, "Invalid regex pattern: \(pattern)")
  152. }
  153. }
  154. // Check if combinedLogs is a valid JSON string. If so, print it as JSON, if not, print it as a string
  155. if let jsonData = "{\(combinedLogs)}".data(using: .utf8) {
  156. do {
  157. let jsonObject = try JSONSerialization.jsonObject(with: jsonData, options: [])
  158. let prettyPrintedData = try JSONSerialization.data(withJSONObject: jsonObject, options: .prettyPrinted)
  159. if let prettyPrintedString = String(data: prettyPrintedData, encoding: .utf8) {
  160. debug(.openAPS, "JavaScript log [JSON]: \(prettyPrintedString)")
  161. }
  162. } catch {
  163. debug(.openAPS, "JavaScript log: \(combinedLogs)")
  164. }
  165. } else {
  166. debug(.openAPS, "JavaScript log: \(combinedLogs)")
  167. }
  168. }
  169. }
  170. @discardableResult func evaluate(script: Script) -> JSValue! {
  171. let result = evaluate(string: script.body)
  172. aggregateLogs()
  173. return result
  174. }
  175. private func evaluate(string: String) -> JSValue! {
  176. let ctx = commonContext ?? createContext()
  177. return ctx.evaluateScript(string)
  178. }
  179. private func json(for string: String) -> RawJSON {
  180. evaluate(string: "JSON.stringify(\(string), null, 4);")!.toString()!
  181. }
  182. func call(function: String, with arguments: [JSON]) -> RawJSON {
  183. let joined = arguments.map(\.rawJSON).joined(separator: ",")
  184. return json(for: "\(function)(\(joined))")
  185. }
  186. func inCommonContext<Value>(execute: (JavaScriptWorker) -> Value) -> Value {
  187. commonContext = createContext()
  188. defer {
  189. commonContext = nil
  190. aggregateLogs()
  191. }
  192. return execute(self)
  193. }
  194. }