@reactiive/ennio 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/EnnioCore.podspec +61 -0
- package/LICENSE +21 -0
- package/README.md +50 -0
- package/android/CMakeLists.txt +40 -0
- package/android/build.gradle +64 -0
- package/cpp/ElementMatcher.cpp +661 -0
- package/cpp/ElementMatcher.hpp +244 -0
- package/cpp/EnnioLog.hpp +182 -0
- package/cpp/HybridEnnio.cpp +1161 -0
- package/cpp/HybridEnnio.hpp +174 -0
- package/cpp/IdleMonitor.hpp +277 -0
- package/cpp/Protocol.cpp +135 -0
- package/cpp/Protocol.hpp +47 -0
- package/cpp/SelectorCriteria.hpp +281 -0
- package/cpp/SelectorParser.cpp +649 -0
- package/cpp/SelectorParser.hpp +94 -0
- package/cpp/ShadowTreeTraverser.cpp +305 -0
- package/cpp/ShadowTreeTraverser.hpp +142 -0
- package/cpp/TestIDRegistry.cpp +109 -0
- package/cpp/TestIDRegistry.hpp +84 -0
- package/dist/cli.js +16221 -0
- package/ios/EnnioAutoInit.mm +338 -0
- package/ios/EnnioDebugBanner.h +19 -0
- package/ios/EnnioDebugBanner.mm +178 -0
- package/ios/EnnioRuntimeHelper.h +264 -0
- package/ios/EnnioRuntimeHelper.mm +2443 -0
- package/lib/Ennio.nitro.d.ts +263 -0
- package/lib/Ennio.nitro.d.ts.map +1 -0
- package/lib/Ennio.nitro.js +2 -0
- package/lib/Ennio.nitro.js.map +1 -0
- package/lib/index.d.ts +16 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +45 -0
- package/lib/index.js.map +1 -0
- package/nitro.json +24 -0
- package/nitrogen/generated/.gitattributes +1 -0
- package/nitrogen/generated/android/EnnioCore+autolinking.cmake +81 -0
- package/nitrogen/generated/android/EnnioCore+autolinking.gradle +27 -0
- package/nitrogen/generated/android/EnnioCoreOnLoad.cpp +49 -0
- package/nitrogen/generated/android/EnnioCoreOnLoad.hpp +34 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/ennio/EnnioCoreOnLoad.kt +35 -0
- package/nitrogen/generated/ios/EnnioCore+autolinking.rb +62 -0
- package/nitrogen/generated/ios/EnnioCore-Swift-Cxx-Bridge.cpp +17 -0
- package/nitrogen/generated/ios/EnnioCore-Swift-Cxx-Bridge.hpp +27 -0
- package/nitrogen/generated/ios/EnnioCore-Swift-Cxx-Umbrella.hpp +38 -0
- package/nitrogen/generated/ios/EnnioCoreAutolinking.mm +35 -0
- package/nitrogen/generated/ios/EnnioCoreAutolinking.swift +16 -0
- package/nitrogen/generated/shared/c++/ExtendedElementInfo.hpp +118 -0
- package/nitrogen/generated/shared/c++/HybridEnnioSpec.cpp +44 -0
- package/nitrogen/generated/shared/c++/HybridEnnioSpec.hpp +93 -0
- package/nitrogen/generated/shared/c++/LayoutMetrics.hpp +103 -0
- package/nitrogen/generated/shared/c++/ScrollDirection.hpp +84 -0
- package/package.json +78 -0
- package/react-native.config.js +14 -0
- package/src/Ennio.nitro.ts +363 -0
- package/src/cli/hid-daemon.py +129 -0
- package/src/index.ts +72 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
// Include the generated spec
|
|
4
|
+
#include "../nitrogen/generated/shared/c++/HybridEnnioSpec.hpp"
|
|
5
|
+
|
|
6
|
+
#include <memory>
|
|
7
|
+
#include <string>
|
|
8
|
+
#include <vector>
|
|
9
|
+
#include <functional>
|
|
10
|
+
#include <mutex>
|
|
11
|
+
|
|
12
|
+
// React Native Fabric headers
|
|
13
|
+
#include <react/renderer/core/ShadowNode.h>
|
|
14
|
+
#include <react/renderer/uimanager/UIManager.h>
|
|
15
|
+
|
|
16
|
+
// JSI for runtime + dispatcher access
|
|
17
|
+
#include <jsi/jsi.h>
|
|
18
|
+
|
|
19
|
+
namespace margelo::nitro { class Dispatcher; }
|
|
20
|
+
|
|
21
|
+
// Internal components
|
|
22
|
+
#include "Protocol.hpp"
|
|
23
|
+
#include "TestIDRegistry.hpp"
|
|
24
|
+
#include "ShadowTreeTraverser.hpp"
|
|
25
|
+
#include "SelectorCriteria.hpp"
|
|
26
|
+
#include "SelectorParser.hpp"
|
|
27
|
+
#include "ElementMatcher.hpp"
|
|
28
|
+
|
|
29
|
+
namespace margelo::nitro::ennio {
|
|
30
|
+
|
|
31
|
+
using ShadowNodePtr = std::shared_ptr<const facebook::react::ShadowNode>;
|
|
32
|
+
using RuntimeExecutor = std::function<void(std::function<void(facebook::jsi::Runtime&)>&&)>;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* HybridEnnio - Main Nitro HybridObject for E2E testing
|
|
36
|
+
*
|
|
37
|
+
* Provides direct access to React Native's Fabric shadow tree
|
|
38
|
+
* for fast, reliable E2E testing without instrumentation.
|
|
39
|
+
*/
|
|
40
|
+
class HybridEnnio : public HybridEnnioSpec {
|
|
41
|
+
public:
|
|
42
|
+
HybridEnnio();
|
|
43
|
+
~HybridEnnio() override = default;
|
|
44
|
+
|
|
45
|
+
// ============================================
|
|
46
|
+
// Element Queries
|
|
47
|
+
// ============================================
|
|
48
|
+
bool exists(const std::string& testID) override;
|
|
49
|
+
bool isVisible(const std::string& testID) override;
|
|
50
|
+
std::variant<nitro::NullType, std::string> getText(const std::string& testID) override;
|
|
51
|
+
|
|
52
|
+
// ============================================
|
|
53
|
+
// Synchronization
|
|
54
|
+
// ============================================
|
|
55
|
+
bool waitForIdle(double timeoutMs) override;
|
|
56
|
+
void synchronize() override;
|
|
57
|
+
|
|
58
|
+
// Wake the moment React fires onCommitFiberRoot, capped at maxMs.
|
|
59
|
+
// Replaces blind sleep settles in the CLI with an early-wake; cap
|
|
60
|
+
// is the safety floor so the worst case is identical to a sleep.
|
|
61
|
+
bool waitForNextCommit(double maxMs) override;
|
|
62
|
+
|
|
63
|
+
// ============================================
|
|
64
|
+
// Selector-based Queries (Full Maestro Parity)
|
|
65
|
+
// ============================================
|
|
66
|
+
std::variant<nitro::NullType, ExtendedElementInfo> findBySelector(const std::string& selectorJson) override;
|
|
67
|
+
std::vector<ExtendedElementInfo> findAllBySelector(const std::string& selectorJson) override;
|
|
68
|
+
bool existsBySelector(const std::string& selectorJson) override;
|
|
69
|
+
std::variant<nitro::NullType, std::string> getTextBySelector(const std::string& selectorJson) override;
|
|
70
|
+
bool isVisibleBySelector(const std::string& selectorJson) override;
|
|
71
|
+
|
|
72
|
+
// ============================================
|
|
73
|
+
// Alert/Modal Handling
|
|
74
|
+
// ============================================
|
|
75
|
+
bool isAlertPresent() override;
|
|
76
|
+
std::string getAlertText() override;
|
|
77
|
+
std::vector<std::string> getAlertButtons() override;
|
|
78
|
+
|
|
79
|
+
// ============================================
|
|
80
|
+
// Fast-mode Writes
|
|
81
|
+
// ============================================
|
|
82
|
+
bool scroll(const std::string& testID, ScrollDirection direction, double distance) override;
|
|
83
|
+
bool scrollTo(const std::string& scrollViewTestID, const std::string& elementTestID) override;
|
|
84
|
+
bool swipeAtPoints(double x1, double y1, double x2, double y2, double durationMs) override;
|
|
85
|
+
bool pressHardwareKey(double keyCode) override;
|
|
86
|
+
bool backGesture() override;
|
|
87
|
+
bool hideKeyboard() override;
|
|
88
|
+
bool tapAlertButton(const std::string& buttonText) override;
|
|
89
|
+
bool dismissAlert() override;
|
|
90
|
+
bool copyToClipboard(const std::string& text) override;
|
|
91
|
+
bool pasteFromClipboard(const std::string& testID) override;
|
|
92
|
+
|
|
93
|
+
// ============================================
|
|
94
|
+
// Initialization (called from JS)
|
|
95
|
+
// ============================================
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Initialize with UIManager reference for shadow tree access
|
|
99
|
+
* Must be called before using query/action methods
|
|
100
|
+
*/
|
|
101
|
+
void initialize(
|
|
102
|
+
std::weak_ptr<facebook::react::UIManager> uiManager,
|
|
103
|
+
facebook::react::SurfaceId surfaceId
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Check if the module is properly initialized
|
|
108
|
+
*/
|
|
109
|
+
bool isInitialized() const;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* JS-thread executor — wraps `RCTInstance.callFunctionOnBufferedRuntimeExecutor:`
|
|
113
|
+
* (or any equivalent scheduler) so background dispatch worker
|
|
114
|
+
* threads can schedule result-writes back onto JS. Stored once
|
|
115
|
+
* during bootstrap by `EnnioAutoInit`.
|
|
116
|
+
*/
|
|
117
|
+
using JSThreadExecutor = std::function<void(std::function<void(facebook::jsi::Runtime&)>&&)>;
|
|
118
|
+
static void setJSThreadExecutor(JSThreadExecutor exec);
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Pure-native bootstrap. Called from `EnnioAutoInit`'s post-start
|
|
122
|
+
* hook on the JS thread (after the runtime is initialised).
|
|
123
|
+
* Captures the runtime, evaluates the commit-signal walker, installs
|
|
124
|
+
* `__ennioDispatch` JSI host function so the external CLI can drive
|
|
125
|
+
* the runner via Hermes Inspector `Runtime.evaluate`. Idempotent.
|
|
126
|
+
*/
|
|
127
|
+
static void nativeBootstrap(facebook::jsi::Runtime& runtime);
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
private:
|
|
131
|
+
// Shadow tree access
|
|
132
|
+
std::weak_ptr<facebook::react::UIManager> uiManager_;
|
|
133
|
+
facebook::react::SurfaceId surfaceId_ = 0;
|
|
134
|
+
mutable std::mutex mutex_;
|
|
135
|
+
|
|
136
|
+
// Screen dimensions for visibility checks
|
|
137
|
+
float screenWidth_ = 0;
|
|
138
|
+
float screenHeight_ = 0;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Get the current shadow tree root for the surface
|
|
142
|
+
*/
|
|
143
|
+
ShadowNodePtr getShadowTreeRoot() const;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Find a node by testID using registry or tree traversal
|
|
147
|
+
*/
|
|
148
|
+
ShadowNodePtr findNode(const std::string& testID) const;
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Run one request through the central command table. Same path is
|
|
152
|
+
* driven from JSI (`__ennioDispatch`) — the CLI hands us a Request
|
|
153
|
+
* over Hermes Inspector CDP and we hand back a Response.
|
|
154
|
+
*/
|
|
155
|
+
::ennio::Response handleCommand(const ::ennio::Request& request);
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Convert internal LayoutMetrics to Nitro struct
|
|
159
|
+
*/
|
|
160
|
+
LayoutMetrics convertLayoutMetrics(const ::ennio::LayoutMetrics& metrics) const;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Convert internal ExtendedElementInfo to Nitro struct
|
|
164
|
+
*/
|
|
165
|
+
ExtendedElementInfo convertExtendedElementInfo(const ::ennio::ExtendedElementInfo& info) const;
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Find a node by selector criteria
|
|
169
|
+
*/
|
|
170
|
+
ShadowNodePtr findNodeBySelector(const ::ennio::SelectorCriteria& criteria) const;
|
|
171
|
+
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
} // namespace margelo::nitro::ennio
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <atomic>
|
|
4
|
+
#include <chrono>
|
|
5
|
+
#include <condition_variable>
|
|
6
|
+
#include <functional>
|
|
7
|
+
#include <mutex>
|
|
8
|
+
|
|
9
|
+
namespace ennio {
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* IdleMonitor tracks pending work to determine when the UI is idle.
|
|
13
|
+
*
|
|
14
|
+
* It monitors:
|
|
15
|
+
* - Shadow tree commits/mounts
|
|
16
|
+
* - JS thread tasks
|
|
17
|
+
* - Network requests (optional)
|
|
18
|
+
*
|
|
19
|
+
* This is a singleton that can be accessed from anywhere in the app.
|
|
20
|
+
*/
|
|
21
|
+
class IdleMonitor {
|
|
22
|
+
public:
|
|
23
|
+
static IdleMonitor& getInstance() {
|
|
24
|
+
static IdleMonitor instance;
|
|
25
|
+
return instance;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Prevent copying
|
|
29
|
+
IdleMonitor(const IdleMonitor&) = delete;
|
|
30
|
+
IdleMonitor& operator=(const IdleMonitor&) = delete;
|
|
31
|
+
|
|
32
|
+
// ============================================
|
|
33
|
+
// Shadow Tree Tracking
|
|
34
|
+
// ============================================
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Called when a shadow tree commit starts
|
|
38
|
+
*/
|
|
39
|
+
void onShadowTreeCommitStart() {
|
|
40
|
+
pendingCommits_.fetch_add(1);
|
|
41
|
+
lastActivityTime_ = std::chrono::steady_clock::now();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Called when a shadow tree commit completes
|
|
46
|
+
*/
|
|
47
|
+
void onShadowTreeCommitEnd() {
|
|
48
|
+
pendingCommits_.fetch_sub(1);
|
|
49
|
+
lastActivityTime_ = std::chrono::steady_clock::now();
|
|
50
|
+
checkAndNotify();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Called when a shadow tree mount starts
|
|
55
|
+
*/
|
|
56
|
+
void onShadowTreeMountStart() {
|
|
57
|
+
pendingMounts_.fetch_add(1);
|
|
58
|
+
lastActivityTime_ = std::chrono::steady_clock::now();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Called when a shadow tree mount completes
|
|
63
|
+
*/
|
|
64
|
+
void onShadowTreeMountEnd() {
|
|
65
|
+
pendingMounts_.fetch_sub(1);
|
|
66
|
+
lastActivityTime_ = std::chrono::steady_clock::now();
|
|
67
|
+
checkAndNotify();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ============================================
|
|
71
|
+
// JS Thread Tracking
|
|
72
|
+
// ============================================
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Called when a JS task starts
|
|
76
|
+
*/
|
|
77
|
+
void onJSTaskStart() {
|
|
78
|
+
pendingJSTasks_.fetch_add(1);
|
|
79
|
+
lastActivityTime_ = std::chrono::steady_clock::now();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Called when a JS task completes
|
|
84
|
+
*/
|
|
85
|
+
void onJSTaskEnd() {
|
|
86
|
+
pendingJSTasks_.fetch_sub(1);
|
|
87
|
+
lastActivityTime_ = std::chrono::steady_clock::now();
|
|
88
|
+
checkAndNotify();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ============================================
|
|
92
|
+
// Network Request Tracking (optional)
|
|
93
|
+
// ============================================
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Called when a network request starts
|
|
97
|
+
*/
|
|
98
|
+
void onNetworkRequestStart() {
|
|
99
|
+
pendingNetworkRequests_.fetch_add(1);
|
|
100
|
+
lastActivityTime_ = std::chrono::steady_clock::now();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Called when a network request completes
|
|
105
|
+
*/
|
|
106
|
+
void onNetworkRequestEnd() {
|
|
107
|
+
pendingNetworkRequests_.fetch_sub(1);
|
|
108
|
+
lastActivityTime_ = std::chrono::steady_clock::now();
|
|
109
|
+
checkAndNotify();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ============================================
|
|
113
|
+
// Animation Tracking
|
|
114
|
+
// ============================================
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Called when an animation starts
|
|
118
|
+
*/
|
|
119
|
+
void onAnimationStart() {
|
|
120
|
+
pendingAnimations_.fetch_add(1);
|
|
121
|
+
lastActivityTime_ = std::chrono::steady_clock::now();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Called when an animation completes
|
|
126
|
+
*/
|
|
127
|
+
void onAnimationEnd() {
|
|
128
|
+
pendingAnimations_.fetch_sub(1);
|
|
129
|
+
lastActivityTime_ = std::chrono::steady_clock::now();
|
|
130
|
+
checkAndNotify();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ============================================
|
|
134
|
+
// Status Queries
|
|
135
|
+
// ============================================
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Check if the UI is currently idle
|
|
139
|
+
* Returns true if:
|
|
140
|
+
* - No pending commits or mounts
|
|
141
|
+
* - No pending JS tasks
|
|
142
|
+
* - (Optionally) No pending network requests
|
|
143
|
+
* - (Optionally) No pending animations
|
|
144
|
+
*/
|
|
145
|
+
bool isIdle(bool includeNetwork = false, bool includeAnimations = false) const {
|
|
146
|
+
if (pendingCommits_.load() > 0) return false;
|
|
147
|
+
if (pendingMounts_.load() > 0) return false;
|
|
148
|
+
if (pendingJSTasks_.load() > 0) return false;
|
|
149
|
+
if (includeNetwork && pendingNetworkRequests_.load() > 0) return false;
|
|
150
|
+
if (includeAnimations && pendingAnimations_.load() > 0) return false;
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Get the time since the last activity
|
|
156
|
+
*/
|
|
157
|
+
std::chrono::milliseconds timeSinceLastActivity() const {
|
|
158
|
+
auto now = std::chrono::steady_clock::now();
|
|
159
|
+
return std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
160
|
+
now - lastActivityTime_.load()
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Wait for idle state with timeout
|
|
166
|
+
* @param timeoutMs Maximum time to wait in milliseconds
|
|
167
|
+
* @param includeNetwork Whether to wait for network requests
|
|
168
|
+
* @param includeAnimations Whether to wait for animations
|
|
169
|
+
* @param stabilityMs How long the system must be idle before returning
|
|
170
|
+
* @return true if idle state was reached, false on timeout
|
|
171
|
+
*/
|
|
172
|
+
bool waitForIdle(
|
|
173
|
+
int timeoutMs,
|
|
174
|
+
bool includeNetwork = false,
|
|
175
|
+
bool includeAnimations = false,
|
|
176
|
+
int stabilityMs = 100
|
|
177
|
+
) {
|
|
178
|
+
auto deadline = std::chrono::steady_clock::now() +
|
|
179
|
+
std::chrono::milliseconds(timeoutMs);
|
|
180
|
+
|
|
181
|
+
std::unique_lock<std::mutex> lock(mutex_);
|
|
182
|
+
|
|
183
|
+
while (std::chrono::steady_clock::now() < deadline) {
|
|
184
|
+
// Check if idle
|
|
185
|
+
if (isIdle(includeNetwork, includeAnimations)) {
|
|
186
|
+
// Check stability - has it been idle long enough?
|
|
187
|
+
auto idleTime = timeSinceLastActivity();
|
|
188
|
+
if (idleTime.count() >= stabilityMs) {
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Wait for remaining stability time
|
|
193
|
+
auto remainingStability = std::chrono::milliseconds(stabilityMs) - idleTime;
|
|
194
|
+
cv_.wait_for(lock, remainingStability);
|
|
195
|
+
} else {
|
|
196
|
+
// Wait for notification or timeout
|
|
197
|
+
auto remaining = std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
198
|
+
deadline - std::chrono::steady_clock::now()
|
|
199
|
+
);
|
|
200
|
+
if (remaining.count() <= 0) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
cv_.wait_for(lock, remaining);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return isIdle(includeNetwork, includeAnimations);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ============================================
|
|
211
|
+
// Debug Information
|
|
212
|
+
// ============================================
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Get current pending counts for debugging
|
|
216
|
+
*/
|
|
217
|
+
struct PendingCounts {
|
|
218
|
+
int commits;
|
|
219
|
+
int mounts;
|
|
220
|
+
int jsTasks;
|
|
221
|
+
int networkRequests;
|
|
222
|
+
int animations;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
PendingCounts getPendingCounts() const {
|
|
226
|
+
return {
|
|
227
|
+
pendingCommits_.load(),
|
|
228
|
+
pendingMounts_.load(),
|
|
229
|
+
pendingJSTasks_.load(),
|
|
230
|
+
pendingNetworkRequests_.load(),
|
|
231
|
+
pendingAnimations_.load()
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Reset all counters (for testing)
|
|
237
|
+
*/
|
|
238
|
+
void reset() {
|
|
239
|
+
pendingCommits_.store(0);
|
|
240
|
+
pendingMounts_.store(0);
|
|
241
|
+
pendingJSTasks_.store(0);
|
|
242
|
+
pendingNetworkRequests_.store(0);
|
|
243
|
+
pendingAnimations_.store(0);
|
|
244
|
+
lastActivityTime_.store(std::chrono::steady_clock::now());
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private:
|
|
248
|
+
IdleMonitor()
|
|
249
|
+
: pendingCommits_(0)
|
|
250
|
+
, pendingMounts_(0)
|
|
251
|
+
, pendingJSTasks_(0)
|
|
252
|
+
, pendingNetworkRequests_(0)
|
|
253
|
+
, pendingAnimations_(0)
|
|
254
|
+
, lastActivityTime_(std::chrono::steady_clock::now()) {}
|
|
255
|
+
|
|
256
|
+
void checkAndNotify() {
|
|
257
|
+
if (isIdle()) {
|
|
258
|
+
cv_.notify_all();
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Atomic counters for thread-safe tracking
|
|
263
|
+
std::atomic<int> pendingCommits_;
|
|
264
|
+
std::atomic<int> pendingMounts_;
|
|
265
|
+
std::atomic<int> pendingJSTasks_;
|
|
266
|
+
std::atomic<int> pendingNetworkRequests_;
|
|
267
|
+
std::atomic<int> pendingAnimations_;
|
|
268
|
+
|
|
269
|
+
// Last activity timestamp
|
|
270
|
+
std::atomic<std::chrono::steady_clock::time_point> lastActivityTime_;
|
|
271
|
+
|
|
272
|
+
// Mutex and condition variable for waiting
|
|
273
|
+
mutable std::mutex mutex_;
|
|
274
|
+
std::condition_variable cv_;
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
} // namespace ennio
|
package/cpp/Protocol.cpp
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#include "Protocol.hpp"
|
|
2
|
+
|
|
3
|
+
#include <iomanip>
|
|
4
|
+
#include <sstream>
|
|
5
|
+
#include <string>
|
|
6
|
+
|
|
7
|
+
namespace ennio {
|
|
8
|
+
|
|
9
|
+
// Escape characters that would otherwise break the toJSON() string when
|
|
10
|
+
// `id` or `error` carries an external value (request id, exception
|
|
11
|
+
// message). Control chars get \uXXXX form, the rest match the strict
|
|
12
|
+
// JSON escape set.
|
|
13
|
+
static std::string jsonEscape(const std::string& str) {
|
|
14
|
+
std::string out;
|
|
15
|
+
out.reserve(str.size() + 8);
|
|
16
|
+
for (char c : str) {
|
|
17
|
+
switch (c) {
|
|
18
|
+
case '"': out += "\\\""; break;
|
|
19
|
+
case '\\': out += "\\\\"; break;
|
|
20
|
+
case '\n': out += "\\n"; break;
|
|
21
|
+
case '\r': out += "\\r"; break;
|
|
22
|
+
case '\t': out += "\\t"; break;
|
|
23
|
+
default:
|
|
24
|
+
if (static_cast<unsigned char>(c) < 0x20) {
|
|
25
|
+
char buf[8];
|
|
26
|
+
snprintf(buf, sizeof(buf), "\\u%04x", static_cast<unsigned char>(c));
|
|
27
|
+
out += buf;
|
|
28
|
+
} else {
|
|
29
|
+
out += c;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
namespace json {
|
|
37
|
+
|
|
38
|
+
std::string stringify(const std::string& key, const std::string& value) {
|
|
39
|
+
return "\"" + key + "\":\"" + value + "\"";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
std::string stringify(const std::string& key, bool value) {
|
|
43
|
+
return "\"" + key + "\":" + (value ? "true" : "false");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
std::string stringify(const std::string& key, int value) {
|
|
47
|
+
return "\"" + key + "\":" + std::to_string(value);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
std::string stringify(const std::string& key, double value) {
|
|
51
|
+
std::ostringstream oss;
|
|
52
|
+
oss << std::setprecision(10) << value;
|
|
53
|
+
return "\"" + key + "\":" + oss.str();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
std::string parseString(const std::string& json, const std::string& key) {
|
|
57
|
+
std::string search = "\"" + key + "\":\"";
|
|
58
|
+
size_t pos = json.find(search);
|
|
59
|
+
if (pos == std::string::npos) {
|
|
60
|
+
return "";
|
|
61
|
+
}
|
|
62
|
+
pos += search.length();
|
|
63
|
+
std::string result;
|
|
64
|
+
bool escaped = false;
|
|
65
|
+
for (size_t i = pos; i < json.size(); i++) {
|
|
66
|
+
char c = json[i];
|
|
67
|
+
if (escaped) {
|
|
68
|
+
switch (c) {
|
|
69
|
+
case '"': result += '"'; break;
|
|
70
|
+
case '\\': result += '\\'; break;
|
|
71
|
+
case 'n': result += '\n'; break;
|
|
72
|
+
case 'r': result += '\r'; break;
|
|
73
|
+
case 't': result += '\t'; break;
|
|
74
|
+
default: result += c; break;
|
|
75
|
+
}
|
|
76
|
+
escaped = false;
|
|
77
|
+
} else if (c == '\\') {
|
|
78
|
+
escaped = true;
|
|
79
|
+
} else if (c == '"') {
|
|
80
|
+
break;
|
|
81
|
+
} else {
|
|
82
|
+
result += c;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
bool parseBool(const std::string& json, const std::string& key) {
|
|
89
|
+
std::string search = "\"" + key + "\":";
|
|
90
|
+
size_t pos = json.find(search);
|
|
91
|
+
if (pos == std::string::npos) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
pos += search.length();
|
|
95
|
+
return json.substr(pos, 4) == "true";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
int parseInt(const std::string& json, const std::string& key) {
|
|
99
|
+
std::string search = "\"" + key + "\":";
|
|
100
|
+
size_t pos = json.find(search);
|
|
101
|
+
if (pos == std::string::npos) {
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
104
|
+
pos += search.length();
|
|
105
|
+
return std::stoi(json.substr(pos));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
double parseDouble(const std::string& json, const std::string& key) {
|
|
109
|
+
std::string search = "\"" + key + "\":";
|
|
110
|
+
size_t pos = json.find(search);
|
|
111
|
+
if (pos == std::string::npos) {
|
|
112
|
+
return 0.0;
|
|
113
|
+
}
|
|
114
|
+
pos += search.length();
|
|
115
|
+
return std::stod(json.substr(pos));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
} // namespace json
|
|
119
|
+
|
|
120
|
+
std::string Response::toJSON() const {
|
|
121
|
+
std::ostringstream oss;
|
|
122
|
+
oss << "{";
|
|
123
|
+
oss << "\"id\":\"" << jsonEscape(id) << "\",";
|
|
124
|
+
oss << "\"success\":" << (success ? "true" : "false");
|
|
125
|
+
if (!data.empty()) {
|
|
126
|
+
oss << ",\"data\":" << data;
|
|
127
|
+
}
|
|
128
|
+
if (!error.empty()) {
|
|
129
|
+
oss << ",\"error\":\"" << jsonEscape(error) << "\"";
|
|
130
|
+
}
|
|
131
|
+
oss << "}";
|
|
132
|
+
return oss.str();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
} // namespace ennio
|
package/cpp/Protocol.hpp
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <string>
|
|
4
|
+
|
|
5
|
+
namespace ennio {
|
|
6
|
+
|
|
7
|
+
// Request envelope. Built by the CLI side as a JSON-RPC payload, hands
|
|
8
|
+
// off to HybridEnnio::handleCommand() inside a background dispatch
|
|
9
|
+
// worker. `payload` is a JSON string (not parsed) — the handler does
|
|
10
|
+
// its own type-specific extraction via cpp/json helpers.
|
|
11
|
+
struct Request {
|
|
12
|
+
std::string id;
|
|
13
|
+
std::string type;
|
|
14
|
+
std::string payload;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Response envelope. `data` is a pre-rendered JSON fragment (object,
|
|
18
|
+
// array, bool, number, string literal — whatever the handler emits).
|
|
19
|
+
// `Response::toJSON()` wraps it into the full reply object the CLI
|
|
20
|
+
// expects: `{"id":..,"success":..,"data":..,"error":..}`.
|
|
21
|
+
struct Response {
|
|
22
|
+
std::string id;
|
|
23
|
+
bool success;
|
|
24
|
+
std::string data;
|
|
25
|
+
std::string error;
|
|
26
|
+
|
|
27
|
+
std::string toJSON() const;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Tiny, dependency-free JSON helpers. Used by the dispatch handlers to
|
|
31
|
+
// peel typed values out of `Request.payload` (which is a JSON string,
|
|
32
|
+
// not a parsed object). Loose enough to handle the CLI's serialiser
|
|
33
|
+
// without pulling in a real JSON library; strict enough to break loudly
|
|
34
|
+
// if the wire format drifts.
|
|
35
|
+
namespace json {
|
|
36
|
+
std::string stringify(const std::string& key, const std::string& value);
|
|
37
|
+
std::string stringify(const std::string& key, bool value);
|
|
38
|
+
std::string stringify(const std::string& key, int value);
|
|
39
|
+
std::string stringify(const std::string& key, double value);
|
|
40
|
+
|
|
41
|
+
std::string parseString(const std::string& json, const std::string& key);
|
|
42
|
+
bool parseBool(const std::string& json, const std::string& key);
|
|
43
|
+
int parseInt(const std::string& json, const std::string& key);
|
|
44
|
+
double parseDouble(const std::string& json, const std::string& key);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
} // namespace ennio
|