| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149 |
- import Foundation
- import JavaScriptCore
- private let contextLock = NSRecursiveLock()
- extension String {
- func replacingRegex(
- matching pattern: String,
- findingOptions: NSRegularExpression.Options = .caseInsensitive,
- replacingOptions: NSRegularExpression.MatchingOptions = [],
- with template: String
- ) throws -> String {
- let regex = try NSRegularExpression(pattern: pattern, options: findingOptions)
- let range = NSRange(startIndex..., in: self)
- return regex.stringByReplacingMatches(in: self, options: replacingOptions, range: range, withTemplate: template)
- }
- }
- extension String {
- var lowercasingFirst: String { prefix(1).lowercased() + dropFirst() }
- var uppercasingFirst: String { prefix(1).uppercased() + dropFirst() }
- var camelCased: String {
- guard !isEmpty else { return "" }
- let parts = components(separatedBy: .alphanumerics.inverted)
- let first = parts.first!.lowercasingFirst
- let rest = parts.dropFirst().map(\.uppercasingFirst)
- return ([first] + rest).joined()
- }
- var pascalCased: String {
- guard !isEmpty else { return "" }
- let parts = components(separatedBy: .alphanumerics.inverted)
- let first = parts.first!.uppercasingFirst
- let rest = parts.dropFirst().map(\.uppercasingFirst)
- return ([first] + rest).joined()
- }
- }
- final class JavaScriptWorker {
- private let processQueue = DispatchQueue(label: "DispatchQueue.JavaScriptWorker")
- private let virtualMachine: JSVirtualMachine
- @SyncAccess(lock: contextLock) private var commonContext: JSContext? = nil
- private var aggregatedLogs: [String] = [] // Step 1: Property to store log messages
- init() {
- virtualMachine = processQueue.sync { JSVirtualMachine()! }
- }
- private func createContext() -> JSContext {
- let context = JSContext(virtualMachine: virtualMachine)!
- context.exceptionHandler = { _, exception in
- if let error = exception?.toString() {
- warning(.openAPS, "JavaScript Error: \(error)")
- }
- }
- let consoleLog: @convention(block) (String) -> Void = { message in
- var parsedMessage = message.trimmingCharacters(in: .whitespacesAndNewlines)
- parsedMessage = try! parsedMessage.replacingRegex(matching: ";", with: ", ")
- parsedMessage = try! parsedMessage.replacingRegex(matching: "\\s?:\\s?,?", with: ": ")
- parsedMessage = try! parsedMessage.replacingRegex(matching: "(\\w+: \\d+(?= [^,:\\s]+:))", with: "$1,")
- parsedMessage = try! parsedMessage.replacingRegex(matching: "^[^\\w]*", with: "")
- parsedMessage = try! parsedMessage.replacingRegex(matching: "(\\sset)?\\sto:?\\s+", with: ": ")
- parsedMessage = try! parsedMessage.replacingRegex(matching: "(\\w+) is (\\w+)\\!?", with: "$1: $2")
- parsedMessage = try! parsedMessage.replacingRegex(matching: "NaN \\(\\. (.+)\\)", with: "$1, ")
- parsedMessage = try! parsedMessage.replacingRegex(matching: "Setting (.+) of (.*)", with: "$1: $2 ")
- parsedMessage = try! parsedMessage.replacingRegex(matching: "(Using\\s|\\sused)", with: "")
- parsedMessage = try! parsedMessage.replacingRegex(
- matching: " instead of past 24 h \\((" + "(-?\\d+(\\.\\d+)?)" + " U)\\)",
- with: "weighted TDD average past 24h: $1"
- )
- parsedMessage = try! parsedMessage.replacingRegex(matching: "^(.+) \\((.+)\\)$", with: "$1: $2")
- parsedMessage = try! parsedMessage.replacingRegex(matching: "\\s?,\\s?$", with: "")
- // Step 2: Split parsedMessage by ',' and, then split by ':' to get the key-value pair
- // Step 3: Convert the key to a camelCased string
- parsedMessage.split(separator: ",").forEach { property in
- let keyPair = property.split(separator: ":")
- if keyPair.count != 2 {
- self.aggregatedLogs.append("\"unknown\": \"\(property)\"")
- return
- }
- let key = keyPair[0].trimmingCharacters(in: .whitespacesAndNewlines).pascalCased
- let value = keyPair[1].trimmingCharacters(in: .whitespacesAndNewlines)
- let keyPairResult = "\"\(key)\": \"\(value)\""
- self.aggregatedLogs.append("\(keyPairResult)")
- }
- }
- context.setObject(
- consoleLog,
- forKeyedSubscript: "_consoleLog" as NSString
- )
- return context
- }
- // New method to flush aggregated logs
- private func aggregateLogs() {
- let combinedLogs = aggregatedLogs.joined(separator: "\n").trimmingCharacters(in: .whitespacesAndNewlines)
- aggregatedLogs.removeAll()
- if !combinedLogs.isEmpty {
- // Check if combinedLogs is a valid JSON string. If so, print it as JSON, if not, print it as a string
- if let jsonData = "{\(combinedLogs)}".data(using: .utf8) {
- do {
- let jsonObject = try JSONSerialization.jsonObject(with: jsonData, options: [])
- _ = try JSONSerialization.data(withJSONObject: jsonObject, options: .prettyPrinted)
- debug(.openAPS, "JavaScript log [JSON]: \n{\n\(combinedLogs)\n}")
- } catch {
- debug(.openAPS, "JavaScript log: \(combinedLogs)")
- }
- } else {
- debug(.openAPS, "JavaScript log: \(combinedLogs)")
- }
- }
- }
- @discardableResult func evaluate(script: Script) -> JSValue! {
- let result = evaluate(string: script.body)
- aggregateLogs()
- return result
- }
- private func evaluate(string: String) -> JSValue! {
- let ctx = commonContext ?? createContext()
- return ctx.evaluateScript(string)
- }
- private func json(for string: String) -> RawJSON {
- evaluate(string: "JSON.stringify(\(string), null, 4);")!.toString()!
- }
- func call(function: String, with arguments: [JSON]) -> RawJSON {
- let joined = arguments.map(\.rawJSON).joined(separator: ",")
- return json(for: "\(function)(\(joined))")
- }
- func inCommonContext<Value>(execute: (JavaScriptWorker) -> Value) -> Value {
- commonContext = createContext()
- defer {
- commonContext = nil
- aggregateLogs()
- }
- return execute(self)
- }
- }
|