JavaScriptWorker.swift 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  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", attributes: .concurrent)
  24. private let virtualMachine: JSVirtualMachine
  25. private var contextPool: [JSContext] = []
  26. private let contextPoolLock = NSLock()
  27. init(poolSize: Int = 5) {
  28. virtualMachine = JSVirtualMachine()!
  29. // Pre-create a pool of JSContext instances
  30. for _ in 0 ..< poolSize {
  31. contextPool.append(createContext())
  32. }
  33. }
  34. private func createContext() -> JSContext {
  35. let context = JSContext(virtualMachine: virtualMachine)!
  36. context.exceptionHandler = { _, exception in
  37. if let error = exception?.toString() {
  38. warning(.openAPS, "JavaScript Error: \(error)")
  39. }
  40. }
  41. let consoleLog: @convention(block) (String) -> Void = { [weak context] message in
  42. guard let context = context else { return }
  43. let trimmedMessage = message.trimmingCharacters(in: .whitespacesAndNewlines)
  44. if !trimmedMessage.isEmpty {
  45. let fileName = context.objectForKeyedSubscript("scriptName").toString() ?? "Unknown"
  46. let threadSafeLog = "\(trimmedMessage)"
  47. self.processQueue.async(flags: .barrier) {
  48. self.outputLogs(for: fileName, message: threadSafeLog)
  49. }
  50. }
  51. }
  52. context.setObject(consoleLog, forKeyedSubscript: "_consoleLog" as NSString)
  53. return context
  54. }
  55. private func getContext() -> JSContext {
  56. contextPoolLock.lock()
  57. let context = contextPool.popLast() ?? createContext()
  58. contextPoolLock.unlock()
  59. return context
  60. }
  61. private func returnContext(_ context: JSContext) {
  62. contextPoolLock.lock()
  63. contextPool.append(context)
  64. contextPoolLock.unlock()
  65. }
  66. private func outputLogs(for fileName: String, message: String) {
  67. let logs = message.trimmingCharacters(in: .whitespacesAndNewlines)
  68. if logs.isEmpty { return }
  69. if fileName == "autosens.js" {
  70. let sanitizedLogs = logs.split(separator: "\n").map { logLine in
  71. logLine.replacingOccurrences(
  72. of: "^[-+=x!]|u\\(|\\)|\\d{1,2}h$",
  73. with: "",
  74. options: .regularExpression
  75. )
  76. }.joined(separator: "\n")
  77. sanitizedLogs.split(separator: "\n").forEach { logLine in
  78. if !logLine.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
  79. debug(.openAPS, "\(fileName): \(logLine)")
  80. }
  81. }
  82. } else {
  83. logs.split(separator: "\n").forEach { logLine in
  84. if !logLine.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
  85. debug(.openAPS, "\(fileName): \(logLine)")
  86. }
  87. }
  88. }
  89. }
  90. @discardableResult func evaluate(script: Script) -> JSValue! {
  91. let context = getContext()
  92. defer { returnContext(context) }
  93. let fileName = URL(fileURLWithPath: script.name).lastPathComponent
  94. context.setObject(fileName, forKeyedSubscript: "scriptName" as NSString)
  95. let result = context.evaluateScript(script.body)
  96. return result
  97. }
  98. private func evaluate(string: String) -> JSValue! {
  99. let context = getContext()
  100. defer { returnContext(context) }
  101. return context.evaluateScript(string)
  102. }
  103. private func json(for string: String) -> RawJSON {
  104. evaluate(string: "JSON.stringify(\(string), null, 4);")!.toString()!
  105. }
  106. func call(function: String, with arguments: [JSON]) -> RawJSON {
  107. let joined = arguments.map(\.rawJSON).joined(separator: ",")
  108. return json(for: "\(function)(\(joined))")
  109. }
  110. func inCommonContext<Value>(execute: (JavaScriptWorker) -> Value) -> Value {
  111. let context = getContext()
  112. defer {
  113. returnContext(context)
  114. }
  115. return execute(self)
  116. }
  117. func evaluateBatch(scripts: [Script]) {
  118. let context = getContext()
  119. defer {
  120. returnContext(context)
  121. }
  122. scripts.forEach { script in
  123. let fileName = URL(fileURLWithPath: script.name).lastPathComponent
  124. context.setObject(fileName, forKeyedSubscript: "scriptName" as NSString)
  125. context.evaluateScript(script.body)
  126. }
  127. }
  128. }