JavaScriptWorker.swift 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  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 consoleLogs: [String] = []
  27. private var logContext: 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. self.consoleLogs.append("\(trimmedMessage)")
  42. }
  43. }
  44. context.setObject(
  45. consoleLog,
  46. forKeyedSubscript: "_consoleLog" as NSString
  47. )
  48. return context
  49. }
  50. // New method to flush aggregated logs
  51. private func outputLogs() {
  52. var outputLogs = consoleLogs.joined(separator: "\n").trimmingCharacters(in: .whitespacesAndNewlines)
  53. consoleLogs.removeAll()
  54. if outputLogs.isEmpty { return }
  55. if logContext == "autosens.js" {
  56. outputLogs = outputLogs.split(separator: "\n").map { logLine in
  57. logLine.replacingOccurrences(
  58. of: "^[-+=x!]|u\\(|\\)|\\d{1,2}h$",
  59. with: "",
  60. options: .regularExpression
  61. )
  62. }.joined(separator: "\n")
  63. }
  64. if !outputLogs.isEmpty {
  65. outputLogs.split(separator: "\n").forEach { logLine in
  66. if !"\(logLine)".trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
  67. debug(.openAPS, "\(logContext): \(logLine)")
  68. }
  69. }
  70. }
  71. }
  72. @discardableResult func evaluate(script: Script) -> JSValue! {
  73. logContext = URL(fileURLWithPath: script.name).lastPathComponent
  74. let result = evaluate(string: script.body)
  75. outputLogs()
  76. return result
  77. }
  78. private func evaluate(string: String) -> JSValue! {
  79. let ctx = commonContext ?? createContext()
  80. return ctx.evaluateScript(string)
  81. }
  82. private func json(for string: String) -> RawJSON {
  83. evaluate(string: "JSON.stringify(\(string), null, 4);")!.toString()!
  84. }
  85. func call(function: String, with arguments: [JSON]) -> RawJSON {
  86. let joined = arguments.map(\.rawJSON).joined(separator: ",")
  87. return json(for: "\(function)(\(joined))")
  88. }
  89. func inCommonContext<Value>(execute: (JavaScriptWorker) -> Value) -> Value {
  90. commonContext = createContext()
  91. defer {
  92. commonContext = nil
  93. outputLogs()
  94. }
  95. return execute(self)
  96. }
  97. }