@capawesome/capacitor-nodejs 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Package.swift +42 -0
- package/README.md +443 -0
- package/android/CMakeLists.txt +32 -0
- package/android/build.gradle +123 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/native-lib.cpp +192 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/nodejs/Nodejs.java +256 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/nodejs/NodejsConfig.java +30 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/nodejs/NodejsPlugin.java +159 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/nodejs/classes/CustomException.java +20 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/nodejs/classes/CustomExceptions.java +23 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/nodejs/classes/events/MessageEvent.java +27 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/nodejs/classes/options/SendOptions.java +48 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/nodejs/classes/options/StartOptions.java +81 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/nodejs/classes/results/IsReadyResult.java +22 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/nodejs/interfaces/Callback.java +5 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/nodejs/interfaces/EmptyCallback.java +5 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/nodejs/interfaces/NonEmptyResultCallback.java +7 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/nodejs/interfaces/Result.java +7 -0
- package/android/src/main/res/.gitkeep +0 -0
- package/dist/docs.json +432 -0
- package/dist/esm/definitions.d.ts +214 -0
- package/dist/esm/definitions.js +38 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +8 -0
- package/dist/esm/web.js +16 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +68 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +71 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Plugin/Assets/builtin_modules/bridge/index.js +205 -0
- package/ios/Plugin/Classes/Events/MessageEvent.swift +19 -0
- package/ios/Plugin/Classes/Options/SendOptions.swift +19 -0
- package/ios/Plugin/Classes/Options/StartOptions.swift +32 -0
- package/ios/Plugin/Classes/Results/IsReadyResult.swift +16 -0
- package/ios/Plugin/Enums/CustomError.swift +46 -0
- package/ios/Plugin/Nodejs.swift +148 -0
- package/ios/Plugin/NodejsConfig.swift +6 -0
- package/ios/Plugin/NodejsPlugin.swift +102 -0
- package/ios/Plugin/Protocols/Result.swift +6 -0
- package/ios/PluginNative/NodeRunner.mm +221 -0
- package/ios/PluginNative/bridge.cpp +225 -0
- package/ios/PluginNative/bridge.h +15 -0
- package/ios/PluginNative/include/NodeRunner.h +12 -0
- package/package.json +93 -0
- package/scripts/postinstall.js +109 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Capacitor
|
|
3
|
+
|
|
4
|
+
@objc public class MessageEvent: NSObject {
|
|
5
|
+
let args: [Any]
|
|
6
|
+
let eventName: String
|
|
7
|
+
|
|
8
|
+
init(eventName: String, args: [Any]) {
|
|
9
|
+
self.args = args
|
|
10
|
+
self.eventName = eventName
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@objc public func toJSObject() -> AnyObject {
|
|
14
|
+
var result = JSObject()
|
|
15
|
+
result["args"] = JSTypes.coerceArrayToJSArray(args) ?? []
|
|
16
|
+
result["eventName"] = eventName
|
|
17
|
+
return result as AnyObject
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Capacitor
|
|
3
|
+
|
|
4
|
+
@objc public class SendOptions: NSObject {
|
|
5
|
+
let args: [Any]
|
|
6
|
+
let eventName: String
|
|
7
|
+
|
|
8
|
+
init(_ call: CAPPluginCall) throws {
|
|
9
|
+
self.args = call.getArray("args") ?? []
|
|
10
|
+
self.eventName = try SendOptions.getEventNameFromCall(call)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
private static func getEventNameFromCall(_ call: CAPPluginCall) throws -> String {
|
|
14
|
+
guard let eventName = call.getString("eventName") else {
|
|
15
|
+
throw CustomError.eventNameMissing
|
|
16
|
+
}
|
|
17
|
+
return eventName
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Capacitor
|
|
3
|
+
|
|
4
|
+
@objc public class StartOptions: NSObject {
|
|
5
|
+
let args: [String]
|
|
6
|
+
let env: [String: String]
|
|
7
|
+
let script: String?
|
|
8
|
+
|
|
9
|
+
override init() {
|
|
10
|
+
self.args = []
|
|
11
|
+
self.env = [:]
|
|
12
|
+
self.script = nil
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
init(_ call: CAPPluginCall) {
|
|
16
|
+
self.args = call.getArray("args", String.self) ?? []
|
|
17
|
+
self.env = StartOptions.getEnvFromCall(call)
|
|
18
|
+
self.script = call.getString("script")
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private static func getEnvFromCall(_ call: CAPPluginCall) -> [String: String] {
|
|
22
|
+
var env: [String: String] = [:]
|
|
23
|
+
if let envObject = call.getObject("env") {
|
|
24
|
+
for (key, value) in envObject {
|
|
25
|
+
if let value = value as? String {
|
|
26
|
+
env[key] = value
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return env
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Capacitor
|
|
3
|
+
|
|
4
|
+
@objc public class IsReadyResult: NSObject, Result {
|
|
5
|
+
let ready: Bool
|
|
6
|
+
|
|
7
|
+
init(ready: Bool) {
|
|
8
|
+
self.ready = ready
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
@objc public func toJSObject() -> AnyObject {
|
|
12
|
+
var result = JSObject()
|
|
13
|
+
result["ready"] = ready
|
|
14
|
+
return result as AnyObject
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
enum CustomError: Error {
|
|
4
|
+
case eventNameMissing
|
|
5
|
+
case nodeAlreadyRunning
|
|
6
|
+
case nodeNotReady
|
|
7
|
+
case projectNotFound
|
|
8
|
+
case scriptNotFound
|
|
9
|
+
case startModeNotManual
|
|
10
|
+
|
|
11
|
+
var code: String? {
|
|
12
|
+
switch self {
|
|
13
|
+
case .eventNameMissing:
|
|
14
|
+
return "EVENT_NAME_MISSING"
|
|
15
|
+
case .nodeAlreadyRunning:
|
|
16
|
+
return "NODE_ALREADY_RUNNING"
|
|
17
|
+
case .nodeNotReady:
|
|
18
|
+
return "NODE_NOT_READY"
|
|
19
|
+
case .projectNotFound:
|
|
20
|
+
return "PROJECT_NOT_FOUND"
|
|
21
|
+
case .scriptNotFound:
|
|
22
|
+
return "SCRIPT_NOT_FOUND"
|
|
23
|
+
case .startModeNotManual:
|
|
24
|
+
return "START_MODE_NOT_MANUAL"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
extension CustomError: LocalizedError {
|
|
30
|
+
public var errorDescription: String? {
|
|
31
|
+
switch self {
|
|
32
|
+
case .eventNameMissing:
|
|
33
|
+
return NSLocalizedString("eventName must be provided.", comment: "eventNameMissing")
|
|
34
|
+
case .nodeAlreadyRunning:
|
|
35
|
+
return NSLocalizedString("The Node.js runtime is already running.", comment: "nodeAlreadyRunning")
|
|
36
|
+
case .nodeNotReady:
|
|
37
|
+
return NSLocalizedString("The Node.js runtime is not ready to receive messages.", comment: "nodeNotReady")
|
|
38
|
+
case .projectNotFound:
|
|
39
|
+
return NSLocalizedString("The Node.js project directory was not found.", comment: "projectNotFound")
|
|
40
|
+
case .scriptNotFound:
|
|
41
|
+
return NSLocalizedString("The script file was not found.", comment: "scriptNotFound")
|
|
42
|
+
case .startModeNotManual:
|
|
43
|
+
return NSLocalizedString("The startMode configuration option must be set to 'manual'.", comment: "startModeNotManual")
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Capacitor
|
|
3
|
+
import NodejsPluginNative
|
|
4
|
+
|
|
5
|
+
@objc public class Nodejs: NSObject {
|
|
6
|
+
private static let channelEvents = "_EVENTS_"
|
|
7
|
+
private static let channelSystem = "_SYSTEM_"
|
|
8
|
+
private static let systemMessageReady = "ready-for-app-events"
|
|
9
|
+
|
|
10
|
+
private let config: NodejsConfig
|
|
11
|
+
private weak var plugin: NodejsPlugin?
|
|
12
|
+
|
|
13
|
+
private var ready = false
|
|
14
|
+
private var started = false
|
|
15
|
+
|
|
16
|
+
init(plugin: NodejsPlugin, config: NodejsConfig) {
|
|
17
|
+
self.config = config
|
|
18
|
+
self.plugin = plugin
|
|
19
|
+
super.init()
|
|
20
|
+
NodeRunner.setMessageHandler { [weak self] channelName, message in
|
|
21
|
+
self?.handleMessage(channelName: channelName, message: message)
|
|
22
|
+
}
|
|
23
|
+
if let dataDirPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first {
|
|
24
|
+
NodeRunner.registerDataDirPath(dataDirPath)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@objc public func isReady() -> Bool {
|
|
29
|
+
return ready
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@objc public func send(_ options: SendOptions) throws {
|
|
33
|
+
guard ready else {
|
|
34
|
+
throw CustomError.nodeNotReady
|
|
35
|
+
}
|
|
36
|
+
guard JSONSerialization.isValidJSONObject(options.args) else {
|
|
37
|
+
throw NSError(domain: "NodejsPlugin", code: 0, userInfo: [NSLocalizedDescriptionKey: "args must be JSON-serializable."])
|
|
38
|
+
}
|
|
39
|
+
let payload = try JSONSerialization.data(withJSONObject: options.args)
|
|
40
|
+
let envelope: [String: Any] = [
|
|
41
|
+
"event": options.eventName,
|
|
42
|
+
"payload": String(data: payload, encoding: .utf8) ?? "[]"
|
|
43
|
+
]
|
|
44
|
+
let envelopeData = try JSONSerialization.data(withJSONObject: envelope)
|
|
45
|
+
guard let message = String(data: envelopeData, encoding: .utf8) else {
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
NodeRunner.sendMessage(Nodejs.channelEvents, message: message)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@objc public func start(_ options: StartOptions, completion: @escaping (_ error: Error?) -> Void) {
|
|
52
|
+
objc_sync_enter(self)
|
|
53
|
+
defer {
|
|
54
|
+
objc_sync_exit(self)
|
|
55
|
+
}
|
|
56
|
+
guard !started else {
|
|
57
|
+
completion(CustomError.nodeAlreadyRunning)
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
started = true
|
|
61
|
+
let thread = Thread { [self] in
|
|
62
|
+
do {
|
|
63
|
+
let projectPath = try self.getProjectPath()
|
|
64
|
+
let builtinModulesPath = self.getBuiltinModulesPath()
|
|
65
|
+
let scriptPath = try self.resolveScriptPath(projectPath: projectPath, script: options.script)
|
|
66
|
+
for (key, value) in options.env {
|
|
67
|
+
setenv(key, value, 1)
|
|
68
|
+
}
|
|
69
|
+
setenv("TMPDIR", NSTemporaryDirectory(), 1)
|
|
70
|
+
var arguments = ["node", scriptPath]
|
|
71
|
+
arguments.append(contentsOf: options.args)
|
|
72
|
+
var nodePath = projectPath
|
|
73
|
+
if let builtinModulesPath = builtinModulesPath {
|
|
74
|
+
nodePath += ":" + builtinModulesPath
|
|
75
|
+
}
|
|
76
|
+
completion(nil)
|
|
77
|
+
NodeRunner.startEngine(withArguments: arguments, nodePath: nodePath)
|
|
78
|
+
} catch {
|
|
79
|
+
self.started = false
|
|
80
|
+
completion(error)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
thread.name = "NodejsPlugin"
|
|
84
|
+
thread.stackSize = 2 * 1024 * 1024
|
|
85
|
+
thread.start()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private func handleMessage(channelName: String, message: String) {
|
|
89
|
+
if channelName == Nodejs.channelSystem {
|
|
90
|
+
if message == Nodejs.systemMessageReady {
|
|
91
|
+
ready = true
|
|
92
|
+
plugin?.notifyReadyListeners()
|
|
93
|
+
}
|
|
94
|
+
} else if channelName == Nodejs.channelEvents {
|
|
95
|
+
guard let envelopeData = message.data(using: .utf8),
|
|
96
|
+
let envelope = try? JSONSerialization.jsonObject(with: envelopeData) as? [String: Any],
|
|
97
|
+
let eventName = envelope["event"] as? String else {
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
var args: [Any] = []
|
|
101
|
+
if let payload = envelope["payload"] as? String, let payloadData = payload.data(using: .utf8) {
|
|
102
|
+
args = (try? JSONSerialization.jsonObject(with: payloadData) as? [Any]) ?? []
|
|
103
|
+
}
|
|
104
|
+
plugin?.notifyMessageListeners(MessageEvent(eventName: eventName, args: args))
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private func getProjectPath() throws -> String {
|
|
109
|
+
let projectPath = Bundle.main.bundlePath + "/public/" + config.nodeDir
|
|
110
|
+
guard FileManager.default.fileExists(atPath: projectPath) else {
|
|
111
|
+
throw CustomError.projectNotFound
|
|
112
|
+
}
|
|
113
|
+
return projectPath
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private func getBuiltinModulesPath() -> String? {
|
|
117
|
+
#if SWIFT_PACKAGE
|
|
118
|
+
return Bundle.module.path(forResource: "builtin_modules", ofType: nil)
|
|
119
|
+
#else
|
|
120
|
+
return Bundle(for: Nodejs.self).path(forResource: "builtin_modules", ofType: nil)
|
|
121
|
+
?? Bundle.main.path(forResource: "builtin_modules", ofType: nil)
|
|
122
|
+
#endif
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private func resolveScriptPath(projectPath: String, script: String?) throws -> String {
|
|
126
|
+
var scriptFile = script
|
|
127
|
+
if scriptFile == nil {
|
|
128
|
+
scriptFile = getMainFromPackageJson(projectPath: projectPath)
|
|
129
|
+
}
|
|
130
|
+
if scriptFile == nil {
|
|
131
|
+
scriptFile = "index.js"
|
|
132
|
+
}
|
|
133
|
+
let scriptPath = projectPath + "/" + (scriptFile ?? "index.js")
|
|
134
|
+
guard FileManager.default.fileExists(atPath: scriptPath) else {
|
|
135
|
+
throw CustomError.scriptNotFound
|
|
136
|
+
}
|
|
137
|
+
return scriptPath
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private func getMainFromPackageJson(projectPath: String) -> String? {
|
|
141
|
+
let packageJsonPath = projectPath + "/package.json"
|
|
142
|
+
guard let packageJsonData = FileManager.default.contents(atPath: packageJsonPath),
|
|
143
|
+
let packageJson = try? JSONSerialization.jsonObject(with: packageJsonData) as? [String: Any] else {
|
|
144
|
+
return nil
|
|
145
|
+
}
|
|
146
|
+
return packageJson["main"] as? String
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Capacitor
|
|
3
|
+
|
|
4
|
+
@objc(NodejsPlugin)
|
|
5
|
+
public class NodejsPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
6
|
+
public let identifier = "NodejsPlugin"
|
|
7
|
+
public let jsName = "Nodejs"
|
|
8
|
+
public let pluginMethods: [CAPPluginMethod] = [
|
|
9
|
+
CAPPluginMethod(name: "isReady", returnType: CAPPluginReturnPromise),
|
|
10
|
+
CAPPluginMethod(name: "send", returnType: CAPPluginReturnPromise),
|
|
11
|
+
CAPPluginMethod(name: "start", returnType: CAPPluginReturnPromise)
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
public static let eventMessage = "message"
|
|
15
|
+
public static let eventReady = "ready"
|
|
16
|
+
public static let startModeAuto = "auto"
|
|
17
|
+
public static let startModeManual = "manual"
|
|
18
|
+
public static let tag = "NodejsPlugin"
|
|
19
|
+
|
|
20
|
+
private var config = NodejsConfig()
|
|
21
|
+
private var implementation: Nodejs?
|
|
22
|
+
|
|
23
|
+
override public func load() {
|
|
24
|
+
config = getNodejsConfig()
|
|
25
|
+
implementation = Nodejs(plugin: self, config: config)
|
|
26
|
+
if config.startMode == NodejsPlugin.startModeAuto {
|
|
27
|
+
implementation?.start(StartOptions()) { error in
|
|
28
|
+
if let error = error {
|
|
29
|
+
CAPLog.print("[", NodejsPlugin.tag, "] ", error)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@objc func isReady(_ call: CAPPluginCall) {
|
|
36
|
+
let result = IsReadyResult(ready: implementation?.isReady() == true)
|
|
37
|
+
resolveCall(call, result)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@objc func send(_ call: CAPPluginCall) {
|
|
41
|
+
do {
|
|
42
|
+
let options = try SendOptions(call)
|
|
43
|
+
|
|
44
|
+
try implementation?.send(options)
|
|
45
|
+
resolveCall(call)
|
|
46
|
+
} catch {
|
|
47
|
+
rejectCall(call, error)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@objc func start(_ call: CAPPluginCall) {
|
|
52
|
+
guard config.startMode == NodejsPlugin.startModeManual else {
|
|
53
|
+
rejectCall(call, CustomError.startModeNotManual)
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
let options = StartOptions(call)
|
|
57
|
+
|
|
58
|
+
implementation?.start(options) { error in
|
|
59
|
+
if let error = error {
|
|
60
|
+
self.rejectCall(call, error)
|
|
61
|
+
} else {
|
|
62
|
+
self.resolveCall(call)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
func notifyMessageListeners(_ event: MessageEvent) {
|
|
68
|
+
notifyListeners(NodejsPlugin.eventMessage, data: event.toJSObject() as? JSObject ?? JSObject(), retainUntilConsumed: true)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
func notifyReadyListeners() {
|
|
72
|
+
notifyListeners(NodejsPlugin.eventReady, data: JSObject(), retainUntilConsumed: true)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private func getNodejsConfig() -> NodejsConfig {
|
|
76
|
+
var config = NodejsConfig()
|
|
77
|
+
if let nodeDir = getConfig().getString("nodeDir") {
|
|
78
|
+
config.nodeDir = nodeDir
|
|
79
|
+
}
|
|
80
|
+
if let startMode = getConfig().getString("startMode") {
|
|
81
|
+
config.startMode = startMode
|
|
82
|
+
}
|
|
83
|
+
return config
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private func rejectCall(_ call: CAPPluginCall, _ error: Error) {
|
|
87
|
+
CAPLog.print("[", NodejsPlugin.tag, "] ", error)
|
|
88
|
+
call.reject(error.localizedDescription, (error as? CustomError)?.code)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private func resolveCall(_ call: CAPPluginCall) {
|
|
92
|
+
call.resolve()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private func resolveCall(_ call: CAPPluginCall, _ result: Result?) {
|
|
96
|
+
if let result = result?.toJSObject() as? JSObject {
|
|
97
|
+
call.resolve(result)
|
|
98
|
+
} else {
|
|
99
|
+
call.resolve()
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Runs the Node.js engine and handles the system channel messages.
|
|
3
|
+
|
|
4
|
+
Based on the `NodeRunner` of the `nodejs-mobile-react-native` project
|
|
5
|
+
(MIT licensed, https://github.com/nodejs-mobile/nodejs-mobile-react-native).
|
|
6
|
+
*/
|
|
7
|
+
#import <UIKit/UIKit.h>
|
|
8
|
+
#import "NodeRunner.h"
|
|
9
|
+
#include <NodeMobile/NodeMobile.h>
|
|
10
|
+
#include <string>
|
|
11
|
+
#include "bridge.h"
|
|
12
|
+
|
|
13
|
+
static NSString *const SYSTEM_CHANNEL = @"_SYSTEM_";
|
|
14
|
+
|
|
15
|
+
static NodeRunnerMessageHandler _messageHandler = nil;
|
|
16
|
+
|
|
17
|
+
// Flag to indicate if the Node.js engine is ready to receive app events.
|
|
18
|
+
static bool nodeIsReadyForAppEvents = false;
|
|
19
|
+
|
|
20
|
+
// Condition to wait on pause event handling on the Node.js side.
|
|
21
|
+
static NSCondition *appEventBeingProcessedCondition = nil;
|
|
22
|
+
|
|
23
|
+
// Set to keep ids for called pause events, so they can be unlocked later.
|
|
24
|
+
static NSMutableSet *appPauseEventsManagerSet = nil;
|
|
25
|
+
|
|
26
|
+
// Lock to manipulate the app pause events manager set.
|
|
27
|
+
static id appPauseEventsManagerSetLock = nil;
|
|
28
|
+
|
|
29
|
+
@interface NodeRunner ()
|
|
30
|
+
|
|
31
|
+
+ (void)handleSystemChannelMessage:(NSString *)msg;
|
|
32
|
+
+ (void)onPause;
|
|
33
|
+
+ (void)onResume;
|
|
34
|
+
+ (void)releasePauseEvent:(NSString *)eventId;
|
|
35
|
+
+ (void)sendPauseEventAndWaitForRelease:(NSDate *)expectedFinishTime;
|
|
36
|
+
|
|
37
|
+
@end
|
|
38
|
+
|
|
39
|
+
static void rcv_message(const char *channelName, const char *msg) {
|
|
40
|
+
@autoreleasepool {
|
|
41
|
+
NSString *objectiveCChannelName = [NSString stringWithUTF8String:channelName];
|
|
42
|
+
NSString *objectiveCMessage = [NSString stringWithUTF8String:msg];
|
|
43
|
+
|
|
44
|
+
if ([objectiveCChannelName isEqualToString:SYSTEM_CHANNEL]) {
|
|
45
|
+
[NodeRunner handleSystemChannelMessage:objectiveCMessage];
|
|
46
|
+
}
|
|
47
|
+
if (_messageHandler) {
|
|
48
|
+
_messageHandler(objectiveCChannelName, objectiveCMessage);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@implementation NodeRunner
|
|
54
|
+
|
|
55
|
+
+ (void)registerDataDirPath:(NSString *)path {
|
|
56
|
+
capacitor_register_node_data_dir_path([path UTF8String]);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
+ (void)sendMessage:(NSString *)channelName message:(NSString *)message {
|
|
60
|
+
capacitor_bridge_notify([channelName UTF8String], [message UTF8String]);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
+ (void)setMessageHandler:(NodeRunnerMessageHandler)handler {
|
|
64
|
+
_messageHandler = handler;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
+ (void)handleSystemChannelMessage:(NSString *)msg {
|
|
68
|
+
if ([msg hasPrefix:@"release-pause-event"]) {
|
|
69
|
+
// The Node.js engine has signaled it has finished handling a pause event.
|
|
70
|
+
// The expected format for this message is "release-pause-event|{eventId}".
|
|
71
|
+
NSArray *eventArguments = [msg componentsSeparatedByString:@"|"];
|
|
72
|
+
if (eventArguments.count >= 2) {
|
|
73
|
+
[NodeRunner releasePauseEvent:eventArguments[1]];
|
|
74
|
+
}
|
|
75
|
+
} else if ([msg isEqualToString:@"ready-for-app-events"]) {
|
|
76
|
+
nodeIsReadyForAppEvents = true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
+ (void)onPause {
|
|
81
|
+
if (!nodeIsReadyForAppEvents) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
UIApplication *application = [UIApplication sharedApplication];
|
|
85
|
+
// Inform the app intends to run something in the background. In this
|
|
86
|
+
// case we'll try to wait for the pause event to be properly taken
|
|
87
|
+
// care of by the Node.js engine.
|
|
88
|
+
__block UIBackgroundTaskIdentifier backgroundWaitForPauseHandlerTask = [application beginBackgroundTaskWithExpirationHandler:^{
|
|
89
|
+
// Expiration handler to avoid app crashes if the task doesn't end
|
|
90
|
+
// in the iOS allowed background duration time.
|
|
91
|
+
[application endBackgroundTask:backgroundWaitForPauseHandlerTask];
|
|
92
|
+
backgroundWaitForPauseHandlerTask = UIBackgroundTaskInvalid;
|
|
93
|
+
}];
|
|
94
|
+
|
|
95
|
+
NSTimeInterval intendedMaxDuration = [application backgroundTimeRemaining] + 1;
|
|
96
|
+
// Calls the event in a background thread, to let the
|
|
97
|
+
// UIApplicationDidEnterBackgroundNotification return as soon as possible.
|
|
98
|
+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
99
|
+
NSDate *targetMaximumFinishTime = [[NSDate date] dateByAddingTimeInterval:intendedMaxDuration];
|
|
100
|
+
[NodeRunner sendPauseEventAndWaitForRelease:targetMaximumFinishTime];
|
|
101
|
+
[application endBackgroundTask:backgroundWaitForPauseHandlerTask];
|
|
102
|
+
backgroundWaitForPauseHandlerTask = UIBackgroundTaskInvalid;
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
+ (void)onResume {
|
|
107
|
+
if (!nodeIsReadyForAppEvents) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
[NodeRunner sendMessage:SYSTEM_CHANNEL message:@"resume"];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Sends the pause event to the Node.js engine and returns only after the
|
|
114
|
+
// engine signals the event has been handled explicitly or the background
|
|
115
|
+
// time is running out.
|
|
116
|
+
+ (void)sendPauseEventAndWaitForRelease:(NSDate *)expectedFinishTime {
|
|
117
|
+
NSString *eventId = [[NSUUID UUID] UUIDString];
|
|
118
|
+
NSString *event = [NSString stringWithFormat:@"pause|%@", eventId];
|
|
119
|
+
|
|
120
|
+
[appEventBeingProcessedCondition lock];
|
|
121
|
+
|
|
122
|
+
@synchronized(appPauseEventsManagerSetLock) {
|
|
123
|
+
[appPauseEventsManagerSet addObject:eventId];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
[NodeRunner sendMessage:SYSTEM_CHANNEL message:event];
|
|
127
|
+
|
|
128
|
+
while (YES) {
|
|
129
|
+
// Looping to avoid unintended spurious wake ups.
|
|
130
|
+
@synchronized(appPauseEventsManagerSetLock) {
|
|
131
|
+
if (![appPauseEventsManagerSet containsObject:eventId]) {
|
|
132
|
+
// The id for this event has been released.
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if ([expectedFinishTime timeIntervalSinceNow] <= 0) {
|
|
137
|
+
// We blocked the background thread long enough.
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
[appEventBeingProcessedCondition waitUntilDate:expectedFinishTime];
|
|
141
|
+
}
|
|
142
|
+
[appEventBeingProcessedCondition unlock];
|
|
143
|
+
|
|
144
|
+
@synchronized(appPauseEventsManagerSetLock) {
|
|
145
|
+
[appPauseEventsManagerSet removeObject:eventId];
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Signals the pause event has been handled by the Node.js side.
|
|
150
|
+
+ (void)releasePauseEvent:(NSString *)eventId {
|
|
151
|
+
[appEventBeingProcessedCondition lock];
|
|
152
|
+
@synchronized(appPauseEventsManagerSetLock) {
|
|
153
|
+
[appPauseEventsManagerSet removeObject:eventId];
|
|
154
|
+
}
|
|
155
|
+
[appEventBeingProcessedCondition broadcast];
|
|
156
|
+
[appEventBeingProcessedCondition unlock];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Node's libuv requires all arguments being on contiguous memory.
|
|
160
|
+
+ (void)startEngineWithArguments:(NSArray<NSString *> *)arguments nodePath:(NSString *)nodePath {
|
|
161
|
+
static dispatch_once_t onceToken;
|
|
162
|
+
dispatch_once(&onceToken, ^{
|
|
163
|
+
appEventBeingProcessedCondition = [[NSCondition alloc] init];
|
|
164
|
+
appPauseEventsManagerSet = [[NSMutableSet alloc] init];
|
|
165
|
+
appPauseEventsManagerSetLock = [[NSObject alloc] init];
|
|
166
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
167
|
+
[[NSNotificationCenter defaultCenter] addObserver:[NodeRunner class]
|
|
168
|
+
selector:@selector(onPause)
|
|
169
|
+
name:UIApplicationDidEnterBackgroundNotification
|
|
170
|
+
object:nil];
|
|
171
|
+
[[NSNotificationCenter defaultCenter] addObserver:[NodeRunner class]
|
|
172
|
+
selector:@selector(onResume)
|
|
173
|
+
name:UIApplicationWillEnterForegroundNotification
|
|
174
|
+
object:nil];
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
setenv("NODE_PATH", [nodePath UTF8String], 1);
|
|
179
|
+
|
|
180
|
+
int c_arguments_size = 0;
|
|
181
|
+
|
|
182
|
+
// Compute the byte size needed for all arguments in contiguous memory.
|
|
183
|
+
for (id argElement in arguments) {
|
|
184
|
+
c_arguments_size += strlen([argElement UTF8String]);
|
|
185
|
+
c_arguments_size++; // for '\0'
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Store the arguments in contiguous memory.
|
|
189
|
+
char *args_buffer = (char *)calloc(c_arguments_size, sizeof(char));
|
|
190
|
+
|
|
191
|
+
// The argv to pass into Node.js.
|
|
192
|
+
char *argv[[arguments count]];
|
|
193
|
+
|
|
194
|
+
// Iterate through the expected start position of each argument in args_buffer.
|
|
195
|
+
char *current_args_position = args_buffer;
|
|
196
|
+
|
|
197
|
+
int argument_count = 0;
|
|
198
|
+
|
|
199
|
+
// Populate the args_buffer and argv.
|
|
200
|
+
for (id argElement in arguments) {
|
|
201
|
+
const char *current_argument = [argElement UTF8String];
|
|
202
|
+
|
|
203
|
+
// Copy the current argument to its expected position in args_buffer.
|
|
204
|
+
strncpy(current_args_position, current_argument, strlen(current_argument));
|
|
205
|
+
|
|
206
|
+
// Save the current argument start position in argv and increment argc.
|
|
207
|
+
argv[argument_count] = current_args_position;
|
|
208
|
+
argument_count++;
|
|
209
|
+
|
|
210
|
+
// Increment to the next argument's expected position.
|
|
211
|
+
current_args_position += strlen(current_args_position) + 1;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
capacitor_register_bridge_cb(rcv_message);
|
|
215
|
+
|
|
216
|
+
// Start Node.js, with argc and argv. This call blocks until the
|
|
217
|
+
// Node.js event loop exits.
|
|
218
|
+
node_start(argument_count, argv);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
@end
|