JavaScriptWorker.swift 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. import Foundation
  2. import JavaScriptCore
  3. private let contextLock = NSRecursiveLock()
  4. extension String {
  5. func replacingRegex(
  6. matching pattern: String,
  7. findingOptions: NSRegularExpression.Options = .caseInsensitive,
  8. replacingOptions: NSRegularExpression.MatchingOptions = [],
  9. with template: String
  10. ) throws -> String {
  11. let regex = try NSRegularExpression(pattern: pattern, options: findingOptions)
  12. let range = NSRange(startIndex..., in: self)
  13. return regex.stringByReplacingMatches(in: self, options: replacingOptions, range: range, withTemplate: template)
  14. }
  15. }
  16. extension String {
  17. var lowercasingFirst: String { prefix(1).lowercased() + dropFirst() }
  18. var uppercasingFirst: String { prefix(1).uppercased() + dropFirst() }
  19. var camelCased: String {
  20. guard !isEmpty else { return "" }
  21. let parts = components(separatedBy: .alphanumerics.inverted)
  22. let first = parts.first!.lowercasingFirst
  23. let rest = parts.dropFirst().map(\.uppercasingFirst)
  24. return ([first] + rest).joined()
  25. }
  26. var pascalCased: String {
  27. guard !isEmpty else { return "" }
  28. let parts = components(separatedBy: .alphanumerics.inverted)
  29. let first = parts.first!.uppercasingFirst
  30. let rest = parts.dropFirst().map(\.uppercasingFirst)
  31. return ([first] + rest).joined()
  32. }
  33. }
  34. final class JavaScriptWorker {
  35. private let processQueue = DispatchQueue(label: "DispatchQueue.JavaScriptWorker")
  36. private let virtualMachine: JSVirtualMachine
  37. @SyncAccess(lock: contextLock) private var commonContext: JSContext? = nil
  38. private var aggregatedLogs: [String] = [] // Step 1: Property to store log messages
  39. init() {
  40. virtualMachine = processQueue.sync { JSVirtualMachine()! }
  41. }
  42. private func createContext() -> JSContext {
  43. let context = JSContext(virtualMachine: virtualMachine)!
  44. context.exceptionHandler = { _, exception in
  45. if let error = exception?.toString() {
  46. warning(.openAPS, "JavaScript Error: \(error)")
  47. }
  48. }
  49. let consoleLog: @convention(block) (String) -> Void = { message in
  50. // Step 1: Format/Leandup the log entry using RegEx
  51. var parsedMessage = message.trimmingCharacters(in: .whitespacesAndNewlines)
  52. parsedMessage = try! parsedMessage.replacingRegex(matching: ";", with: ", ")
  53. parsedMessage = try! parsedMessage.replacingRegex(matching: "\\s?:\\s?,?", with: ": ")
  54. parsedMessage = try! parsedMessage.replacingRegex(matching: "(\\w+: \\d+(?= [^,:\\s]+:))", with: "$1,")
  55. parsedMessage = try! parsedMessage.replacingRegex(matching: "^[^\\w]*", with: "")
  56. parsedMessage = try! parsedMessage.replacingRegex(matching: "(\\sset)?\\sto:?\\s+", with: ": ")
  57. parsedMessage = try! parsedMessage.replacingRegex(matching: "(\\w+) is (\\w+)\\!?", with: "$1: $2")
  58. parsedMessage = try! parsedMessage.replacingRegex(matching: "NaN \\(\\. (.+)\\)", with: "$1, ")
  59. parsedMessage = try! parsedMessage.replacingRegex(matching: "Setting (.+) of (.*)", with: "$1: $2 ")
  60. parsedMessage = try! parsedMessage.replacingRegex(matching: "(Using\\s|\\sused)", with: "")
  61. parsedMessage = try! parsedMessage.replacingRegex(
  62. matching: " instead of past 24 h \\((" + "(-?\\d+(\\.\\d+)?)" + " U)\\)",
  63. with: "weighted TDD average past 24h: $1"
  64. )
  65. parsedMessage = try! parsedMessage.replacingRegex(matching: "^(.+) \\((.+)\\)$", with: "$1: $2")
  66. parsedMessage = try! parsedMessage.replacingRegex(matching: "\\s?,\\s?$", with: "")
  67. // Step 2: Split parsedMessage by ',' and, then split by ':' to get the key-value pair
  68. var keyPairResults = " "
  69. parsedMessage.split(separator: ",").forEach { property in
  70. let keyPair = property.split(separator: ":")
  71. if keyPair.count != 2 {
  72. keyPairResults += "\"unknown\": \"\(property)\", "
  73. } else {
  74. // Step 3: Convert the key to a PascalCased string
  75. let key = keyPair[0].trimmingCharacters(in: .whitespacesAndNewlines).pascalCased
  76. let value = keyPair[1].trimmingCharacters(in: .whitespacesAndNewlines)
  77. keyPairResults += "\"\(key)\": \"\(value)\", "
  78. }
  79. }
  80. self.aggregatedLogs.append("\(keyPairResults)")
  81. }
  82. context.setObject(
  83. consoleLog,
  84. forKeyedSubscript: "_consoleLog" as NSString
  85. )
  86. return context
  87. }
  88. // New method to flush aggregated logs
  89. private func aggregateLogs() {
  90. let combinedLogs = aggregatedLogs.joined(separator: "\n")
  91. aggregatedLogs.removeAll()
  92. if !combinedLogs.isEmpty {
  93. // Check if combinedLogs is a valid JSON string. If so, print it as JSON, if not, print it as a string
  94. if let jsonData = "{\(combinedLogs)}".data(using: .utf8) {
  95. do {
  96. let jsonObject = try JSONSerialization.jsonObject(with: jsonData, options: [])
  97. _ = try JSONSerialization.data(withJSONObject: jsonObject, options: .prettyPrinted)
  98. debug(.openAPS, "JavaScript log [JSON]: \n{\n\(combinedLogs)\n}")
  99. } catch {
  100. debug(.openAPS, "JavaScript log: \(combinedLogs)")
  101. }
  102. } else {
  103. debug(.openAPS, "JavaScript log: \(combinedLogs)")
  104. }
  105. }
  106. }
  107. @discardableResult func evaluate(script: Script) -> JSValue! {
  108. let result = evaluate(string: script.body)
  109. aggregateLogs()
  110. return result
  111. }
  112. private func evaluate(string: String) -> JSValue! {
  113. let ctx = commonContext ?? createContext()
  114. return ctx.evaluateScript(string)
  115. }
  116. private func json(for string: String) -> RawJSON {
  117. evaluate(string: "JSON.stringify(\(string), null, 4);")!.toString()!
  118. }
  119. func call(function: String, with arguments: [JSON]) -> RawJSON {
  120. let joined = arguments.map(\.rawJSON).joined(separator: ",")
  121. return json(for: "\(function)(\(joined))")
  122. }
  123. func inCommonContext<Value>(execute: (JavaScriptWorker) -> Value) -> Value {
  124. commonContext = createContext()
  125. defer {
  126. commonContext = nil
  127. aggregateLogs()
  128. }
  129. return execute(self)
  130. }
  131. }