JavaScriptWorker.swift 7.0 KB

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