react-native-stallion 2.3.0-alpha.4 → 2.3.0-alpha.6
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/android/build.gradle +25 -0
- package/android/src/main/cpp/CMakeLists.txt +19 -0
- package/android/src/main/cpp/stallion_signal_handler.cpp +91 -0
- package/android/src/main/java/com/stallion/Stallion.java +0 -2
- package/android/src/main/java/com/stallion/StallionModule.java +37 -1
- package/android/src/main/java/com/stallion/events/StallionEventConstants.java +1 -0
- package/android/src/main/java/com/stallion/events/StallionEventManager.java +23 -15
- package/android/src/main/java/com/stallion/networkmanager/StallionFileDownloader.java +8 -5
- package/android/src/main/java/com/stallion/networkmanager/StallionSyncHandler.java +18 -1
- package/android/src/main/java/com/stallion/storage/StallionMeta.java +52 -1
- package/android/src/main/java/com/stallion/storage/StallionStateManager.java +22 -1
- package/android/src/main/java/com/stallion/utils/StallionDeviceInfo.java +83 -0
- package/android/src/main/java/com/stallion/utils/StallionExceptionHandler.java +162 -29
- package/ios/Stallion.xcodeproj/project.pbxproj +6 -0
- package/ios/main/Stallion.swift +20 -0
- package/ios/main/StallionConstants.swift +1 -0
- package/ios/main/StallionDeviceInfo.swift +105 -0
- package/ios/main/StallionEventHandler.m +3 -1
- package/ios/main/StallionExceptionHandler.h +1 -0
- package/ios/main/StallionExceptionHandler.mm +379 -0
- package/ios/main/StallionFileDownloader.swift +9 -1
- package/ios/main/StallionMeta.h +11 -0
- package/ios/main/StallionMeta.m +76 -1
- package/ios/main/StallionSlotManager.m +2 -2
- package/ios/main/StallionStateManager.m +29 -0
- package/ios/main/StallionSyncHandler.swift +14 -2
- package/package.json +1 -1
- package/react-native-stallion.podspec +22 -0
- package/src/main/components/modules/listing/components/BundleCardInfoSection.js +1 -1
- package/src/main/components/modules/listing/components/styles/index.js +18 -13
- package/src/main/components/modules/listing/components/styles/index.js.map +1 -1
- package/src/main/components/modules/listing/index.js +15 -15
- package/src/main/components/modules/listing/index.js.map +1 -1
- package/src/main/components/modules/listing/styles.js +2 -1
- package/src/main/components/modules/listing/styles.js.map +1 -1
- package/src/main/components/modules/login/index.js +8 -5
- package/src/main/components/modules/login/index.js.map +1 -1
- package/src/main/components/modules/login/styles/index.js +10 -4
- package/src/main/components/modules/login/styles/index.js.map +1 -1
- package/src/main/components/modules/modal/StallionModal.js +2 -2
- package/src/main/components/modules/modal/StallionModal.js.map +1 -1
- package/src/main/components/modules/prod/prod.js +5 -4
- package/src/main/components/modules/prod/prod.js.map +1 -1
- package/src/main/components/modules/prod/styles/index.js +7 -4
- package/src/main/components/modules/prod/styles/index.js.map +1 -1
- package/src/main/constants/appConstants.js +1 -0
- package/src/main/constants/appConstants.js.map +1 -1
- package/src/main/state/actionCreators/useUpdateMetaActions.js +2 -2
- package/src/main/state/actionCreators/useUpdateMetaActions.js.map +1 -1
- package/src/main/state/index.js +5 -3
- package/src/main/state/index.js.map +1 -1
- package/src/main/state/reducers/updateMetaReducer.js +8 -0
- package/src/main/state/reducers/updateMetaReducer.js.map +1 -1
- package/src/main/state/useStallionEvents.js +27 -4
- package/src/main/state/useStallionEvents.js.map +1 -1
- package/src/main/utils/ErrorBoundary.js +45 -11
- package/src/main/utils/ErrorBoundary.js.map +1 -1
- package/src/main/utils/crashState.js +16 -0
- package/src/main/utils/crashState.js.map +1 -0
- package/src/main/utils/useStallionUpdate.js +2 -2
- package/src/main/utils/useStallionUpdate.js.map +1 -1
- package/src/main/utils/withStallion.js +4 -2
- package/src/main/utils/withStallion.js.map +1 -1
- package/src/types/updateMeta.types.js +1 -0
- package/src/types/updateMeta.types.js.map +1 -1
- package/types/main/components/modules/listing/components/styles/index.d.ts +9 -4
- package/types/main/components/modules/listing/components/styles/index.d.ts.map +1 -1
- package/types/main/components/modules/listing/index.d.ts.map +1 -1
- package/types/main/components/modules/listing/styles.d.ts +1 -0
- package/types/main/components/modules/listing/styles.d.ts.map +1 -1
- package/types/main/components/modules/login/index.d.ts.map +1 -1
- package/types/main/components/modules/login/styles/index.d.ts +6 -0
- package/types/main/components/modules/login/styles/index.d.ts.map +1 -1
- package/types/main/components/modules/prod/prod.d.ts.map +1 -1
- package/types/main/components/modules/prod/styles/index.d.ts +5 -2
- package/types/main/components/modules/prod/styles/index.d.ts.map +1 -1
- package/types/main/constants/appConstants.d.ts +2 -0
- package/types/main/constants/appConstants.d.ts.map +1 -1
- package/types/main/index.d.ts +1 -1
- package/types/main/state/actionCreators/useUpdateMetaActions.d.ts.map +1 -1
- package/types/main/state/index.d.ts +4 -1
- package/types/main/state/index.d.ts.map +1 -1
- package/types/main/state/reducers/updateMetaReducer.d.ts +1 -0
- package/types/main/state/reducers/updateMetaReducer.d.ts.map +1 -1
- package/types/main/state/useStallionEvents.d.ts +4 -2
- package/types/main/state/useStallionEvents.d.ts.map +1 -1
- package/types/main/utils/ErrorBoundary.d.ts +2 -1
- package/types/main/utils/ErrorBoundary.d.ts.map +1 -1
- package/types/main/utils/crashState.d.ts +4 -0
- package/types/main/utils/crashState.d.ts.map +1 -0
- package/types/main/utils/useStallionUpdate.d.ts.map +1 -1
- package/types/main/utils/withStallion.d.ts +2 -1
- package/types/main/utils/withStallion.d.ts.map +1 -1
- package/types/types/updateMeta.types.d.ts +7 -2
- package/types/types/updateMeta.types.d.ts.map +1 -1
- package/types/types/utils.types.d.ts +1 -1
- package/types/types/utils.types.d.ts.map +1 -1
- package/ios/main/StallionExceptionHandler.m +0 -184
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
//
|
|
2
|
+
// StallionExceptionHandler.mm
|
|
3
|
+
// react-native-stallion
|
|
4
|
+
//
|
|
5
|
+
// Created by Thor963 on 29/01/25.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
#import "StallionExceptionHandler.h"
|
|
9
|
+
#import "StallionStateManager.h"
|
|
10
|
+
#import "StallionEventHandler.h"
|
|
11
|
+
#import "StallionSlotManager.h"
|
|
12
|
+
#import "StallionMeta.h"
|
|
13
|
+
#import "StallionMetaConstants.h"
|
|
14
|
+
#import "StallionObjConstants.h"
|
|
15
|
+
#import "StallionFileManager.h"
|
|
16
|
+
#import <signal.h>
|
|
17
|
+
#import <unistd.h>
|
|
18
|
+
#import <fcntl.h>
|
|
19
|
+
#import <React/RCTLog.h>
|
|
20
|
+
#import <React/RCTUtils.h>
|
|
21
|
+
#include <atomic>
|
|
22
|
+
|
|
23
|
+
// Forward declarations
|
|
24
|
+
void handleException(NSException *exception);
|
|
25
|
+
void handleSignal(int signal, siginfo_t *info, void *context);
|
|
26
|
+
static void performStallionRollback(NSString *errorString);
|
|
27
|
+
static void setupSignalHandlers(void);
|
|
28
|
+
static void processNativeCrashMarkerIfPresent(void);
|
|
29
|
+
|
|
30
|
+
// Async-signal-safe crash marker storage
|
|
31
|
+
static char g_marker_path[512];
|
|
32
|
+
static char g_mount_marker_path[512];
|
|
33
|
+
|
|
34
|
+
@implementation StallionExceptionHandler
|
|
35
|
+
|
|
36
|
+
NSUncaughtExceptionHandler *_defaultExceptionHandler;
|
|
37
|
+
static std::atomic<bool> exceptionAlertDismissed(false);
|
|
38
|
+
static std::atomic<bool> exceptionAlertShowing(false);
|
|
39
|
+
static std::atomic<bool> rollbackPerformed(false);
|
|
40
|
+
static std::atomic<bool> hasProcessedNativeCrashMarker(false);
|
|
41
|
+
static struct sigaction _previousHandlers[32]; // Store previous handlers for chaining
|
|
42
|
+
|
|
43
|
+
+ (void)initExceptionHandler {
|
|
44
|
+
static dispatch_once_t onceToken;
|
|
45
|
+
dispatch_once(&onceToken, ^{
|
|
46
|
+
// Reset rollback flag when initializing exception handler
|
|
47
|
+
[self resetRollbackFlag];
|
|
48
|
+
|
|
49
|
+
// Store and set Objective-C exception handler
|
|
50
|
+
if (!_defaultExceptionHandler) {
|
|
51
|
+
_defaultExceptionHandler = NSGetUncaughtExceptionHandler();
|
|
52
|
+
}
|
|
53
|
+
NSSetUncaughtExceptionHandler(&handleException);
|
|
54
|
+
|
|
55
|
+
// Setup signal handlers using sigaction with chaining
|
|
56
|
+
setupSignalHandlers();
|
|
57
|
+
|
|
58
|
+
// Initialize JavaScript exception handler
|
|
59
|
+
[self initJavaScriptExceptionHandler];
|
|
60
|
+
|
|
61
|
+
// Process any crash marker from previous session
|
|
62
|
+
processNativeCrashMarkerIfPresent();
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
+ (void)initJavaScriptExceptionHandler {
|
|
67
|
+
// Handle fatal JavaScript errors that cause app crashes
|
|
68
|
+
RCTSetLogFunction(^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) {
|
|
69
|
+
// Rollback only for fatal errors that crash the app
|
|
70
|
+
if (level >= RCTLogLevelFatal) {
|
|
71
|
+
NSString *errorString = [NSString stringWithFormat:@"Fatal JS Error in %@:%@ - %@", fileName, lineNumber, message];
|
|
72
|
+
performStallionRollback(errorString);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
+ (void)resetRollbackFlag {
|
|
78
|
+
rollbackPerformed.store(false);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@end
|
|
82
|
+
|
|
83
|
+
#pragma mark - Shared Rollback Logic
|
|
84
|
+
|
|
85
|
+
static void performStallionRollback(NSString *errorString) {
|
|
86
|
+
|
|
87
|
+
StallionStateManager *stateManager = [StallionStateManager sharedInstance];
|
|
88
|
+
StallionMeta *meta = stateManager.stallionMeta;
|
|
89
|
+
BOOL isAutoRollback = !stateManager.isMounted;
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
// Only prevent multiple executions for auto rollback cases
|
|
93
|
+
// Launch crashes (when mounted) can continue to be registered
|
|
94
|
+
if (isAutoRollback) {
|
|
95
|
+
// Use compare-and-swap to atomically check and set the flag
|
|
96
|
+
bool expected = false;
|
|
97
|
+
if (!rollbackPerformed.compare_exchange_strong(expected, true)) {
|
|
98
|
+
// Flag was already true, skip duplicate rollback
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (errorString.length > 900) {
|
|
104
|
+
errorString = [errorString substringToIndex:900];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (meta.switchState == SwitchStateProd) {
|
|
108
|
+
NSString *currentHash = [meta getActiveReleaseHash] ?: @"";
|
|
109
|
+
|
|
110
|
+
[[StallionEventHandler sharedInstance] cacheEvent:StallionObjConstants.exception_prod_event
|
|
111
|
+
eventPayload:@{
|
|
112
|
+
@"meta": errorString,
|
|
113
|
+
StallionObjConstants.release_hash_key: currentHash,
|
|
114
|
+
StallionObjConstants.is_auto_rollback_key: isAutoRollback ? @"true" : @"false"
|
|
115
|
+
}];
|
|
116
|
+
if (isAutoRollback) {
|
|
117
|
+
[StallionSlotManager rollbackProdWithAutoRollback:YES errorString:errorString];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
} else if (meta.switchState == SwitchStateStage) {
|
|
121
|
+
NSString *currentStageHash = meta.stageNewHash ?: @"";
|
|
122
|
+
|
|
123
|
+
// Emit exception event before rollback (consistent with PROD and Android)
|
|
124
|
+
[[StallionEventHandler sharedInstance] cacheEvent:StallionObjConstants.exception_stage_event
|
|
125
|
+
eventPayload:@{
|
|
126
|
+
@"meta": errorString,
|
|
127
|
+
StallionObjConstants.release_hash_key: currentStageHash,
|
|
128
|
+
StallionObjConstants.is_auto_rollback_key: isAutoRollback ? @"true" : @"false"
|
|
129
|
+
}];
|
|
130
|
+
|
|
131
|
+
if(isAutoRollback) {
|
|
132
|
+
[StallionSlotManager rollbackStage];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Check if alert is already showing (atomic check to prevent duplicates)
|
|
136
|
+
bool expectedShowing = false;
|
|
137
|
+
if (exceptionAlertShowing.compare_exchange_strong(expectedShowing, true)) {
|
|
138
|
+
// We successfully claimed the right to show the alert
|
|
139
|
+
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Stallion Exception Handler"
|
|
140
|
+
message:[NSString stringWithFormat:@"%@\n%@",
|
|
141
|
+
@"A crash occurred in the app. Build was rolled back. Check crash report below. Continue crash to invoke other exception handlers. \n \n",
|
|
142
|
+
errorString]
|
|
143
|
+
preferredStyle:UIAlertControllerStyleAlert];
|
|
144
|
+
|
|
145
|
+
[alert addAction:[UIAlertAction actionWithTitle:@"Continue Crash"
|
|
146
|
+
style:UIAlertActionStyleDefault
|
|
147
|
+
handler:^(UIAlertAction *action) {
|
|
148
|
+
// Set flag to true only when button is pressed
|
|
149
|
+
exceptionAlertDismissed.store(true);
|
|
150
|
+
}]];
|
|
151
|
+
|
|
152
|
+
UIApplication *app = [UIApplication sharedApplication];
|
|
153
|
+
UIViewController *rootViewController = app.delegate.window.rootViewController;
|
|
154
|
+
|
|
155
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
156
|
+
[rootViewController presentViewController:alert animated:YES completion:nil];
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Wait for user to press the button (exceptionAlertDismissed becomes true)
|
|
160
|
+
while (!exceptionAlertDismissed.load()) {
|
|
161
|
+
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
#pragma mark - Signal Handler Setup
|
|
168
|
+
|
|
169
|
+
// Async-signal-safe function to check if mount marker exists
|
|
170
|
+
static int is_mounted_safe(void) {
|
|
171
|
+
int fd = open(g_mount_marker_path, O_RDONLY);
|
|
172
|
+
if (fd >= 0) {
|
|
173
|
+
close(fd);
|
|
174
|
+
return 1; // Mounted
|
|
175
|
+
}
|
|
176
|
+
return 0; // Not mounted
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Async-signal-safe JSON writing (minimal JSON for crash marker)
|
|
180
|
+
static void write_crash_marker_json_safe(int signal, int mounted) {
|
|
181
|
+
int fd = open(g_marker_path, O_CREAT | O_WRONLY | O_TRUNC, 0600);
|
|
182
|
+
if (fd >= 0) {
|
|
183
|
+
// Write JSON: {"signal":X,"isAutoRollback":true/false,"crashLog":"signal=X\n"}
|
|
184
|
+
// isAutoRollback = !mounted (auto rollback if not mounted)
|
|
185
|
+
int autoRollback = !mounted;
|
|
186
|
+
char json[512];
|
|
187
|
+
int len = snprintf(json, sizeof(json),
|
|
188
|
+
"{\"signal\":%d,\"isAutoRollback\":%s,\"crashLog\":\"signal=%d\\n\"}",
|
|
189
|
+
signal, autoRollback ? "true" : "false", signal);
|
|
190
|
+
if (len > 0 && len < (int)sizeof(json)) {
|
|
191
|
+
(void)write(fd, json, len);
|
|
192
|
+
}
|
|
193
|
+
close(fd);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
static void setupSignalHandlers(void) {
|
|
198
|
+
// Initialize marker paths from StallionStateManager
|
|
199
|
+
StallionStateManager *stateManager = [StallionStateManager sharedInstance];
|
|
200
|
+
NSString *filesDir = stateManager.stallionConfig.filesDirectory;
|
|
201
|
+
if (filesDir) {
|
|
202
|
+
const char *path = [filesDir UTF8String];
|
|
203
|
+
snprintf(g_marker_path, sizeof(g_marker_path), "%s/%s", path, "stallion_crash.marker");
|
|
204
|
+
snprintf(g_mount_marker_path, sizeof(g_mount_marker_path), "%s/%s", path, "stallion_mount.marker");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
struct sigaction action;
|
|
208
|
+
sigemptyset(&action.sa_mask);
|
|
209
|
+
action.sa_flags = SA_SIGINFO;
|
|
210
|
+
action.sa_sigaction = handleSignal;
|
|
211
|
+
|
|
212
|
+
// List of signals to catch - comprehensive coverage
|
|
213
|
+
int signals[] = {
|
|
214
|
+
SIGABRT, // Abort signal
|
|
215
|
+
SIGILL, // Illegal instruction
|
|
216
|
+
SIGSEGV, // Segmentation violation
|
|
217
|
+
SIGFPE, // Floating point exception
|
|
218
|
+
SIGBUS, // Bus error
|
|
219
|
+
SIGTRAP, // Trace/breakpoint trap
|
|
220
|
+
SIGPIPE, // Broken pipe
|
|
221
|
+
SIGSYS, // Bad system call
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
int signalCount = sizeof(signals) / sizeof(signals[0]);
|
|
225
|
+
|
|
226
|
+
for (int i = 0; i < signalCount; i++) {
|
|
227
|
+
int sig = signals[i];
|
|
228
|
+
// Store previous handler before installing ours (for chaining)
|
|
229
|
+
sigaction(sig, NULL, &_previousHandlers[sig]);
|
|
230
|
+
// Now install our handler
|
|
231
|
+
sigaction(sig, &action, NULL);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
void handleException(NSException *exception) {
|
|
236
|
+
NSString *readableError = [exception reason] ?: @"Unknown exception";
|
|
237
|
+
NSString *name = [exception name] ?: @"Unknown";
|
|
238
|
+
NSString *callStack = [[exception callStackSymbols] componentsJoinedByString:@"\n"];
|
|
239
|
+
NSString *fullError = [NSString stringWithFormat:@"Exception: %@\nReason: %@\nStack:\n%@", name, readableError, callStack];
|
|
240
|
+
|
|
241
|
+
performStallionRollback(fullError);
|
|
242
|
+
|
|
243
|
+
// Chain to default exception handler if available
|
|
244
|
+
if (_defaultExceptionHandler) {
|
|
245
|
+
_defaultExceptionHandler(exception);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
void handleSignal(int signalVal, siginfo_t *info, void *context) {
|
|
250
|
+
|
|
251
|
+
if (!rollbackPerformed.load()) {
|
|
252
|
+
// Async-signal-safe operations only
|
|
253
|
+
// Read mount state at crash time (async-signal-safe)
|
|
254
|
+
int mounted = is_mounted_safe();
|
|
255
|
+
// Write JSON marker with crash info and autoRollback flag
|
|
256
|
+
write_crash_marker_json_safe(signalVal, mounted);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Chain to previous handler if it exists and is valid (bubble up)
|
|
260
|
+
if (signalVal >= 0 && signalVal < 32) {
|
|
261
|
+
struct sigaction *prev = &_previousHandlers[signalVal];
|
|
262
|
+
if (prev->sa_handler != SIG_DFL && prev->sa_handler != SIG_IGN && prev->sa_handler != NULL) {
|
|
263
|
+
// Prevent infinite loop - don't call ourselves
|
|
264
|
+
if (prev->sa_sigaction != handleSignal) {
|
|
265
|
+
if (prev->sa_flags & SA_SIGINFO) {
|
|
266
|
+
prev->sa_sigaction(signalVal, info, context);
|
|
267
|
+
} else if (prev->sa_handler != SIG_DFL && prev->sa_handler != SIG_IGN) {
|
|
268
|
+
prev->sa_handler(signalVal);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Restore default and raise to proceed with crash
|
|
275
|
+
signal(signalVal, SIG_DFL);
|
|
276
|
+
raise(signalVal);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
#pragma mark - Crash Marker Processing
|
|
280
|
+
|
|
281
|
+
static void processNativeCrashMarkerIfPresent(void) {
|
|
282
|
+
@try {
|
|
283
|
+
if (hasProcessedNativeCrashMarker.load()) {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
StallionStateManager *stateManager = [StallionStateManager sharedInstance];
|
|
288
|
+
NSString *filesDir = stateManager.stallionConfig.filesDirectory;
|
|
289
|
+
NSString *markerPath = [NSString stringWithFormat:@"%@/stallion_crash.marker", filesDir];
|
|
290
|
+
|
|
291
|
+
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
292
|
+
if ([fileManager fileExistsAtPath:markerPath]) {
|
|
293
|
+
NSError *error = nil;
|
|
294
|
+
NSString *jsonContent = [NSString stringWithContentsOfFile:markerPath
|
|
295
|
+
encoding:NSUTF8StringEncoding
|
|
296
|
+
error:&error];
|
|
297
|
+
|
|
298
|
+
if (jsonContent && !error) {
|
|
299
|
+
NSString *stackTraceString = @"";
|
|
300
|
+
BOOL isAutoRollback = NO;
|
|
301
|
+
|
|
302
|
+
@try {
|
|
303
|
+
// Parse JSON from previous crash
|
|
304
|
+
NSData *jsonData = [jsonContent dataUsingEncoding:NSUTF8StringEncoding];
|
|
305
|
+
NSDictionary *crashMarker = [NSJSONSerialization JSONObjectWithData:jsonData
|
|
306
|
+
options:0
|
|
307
|
+
error:&error];
|
|
308
|
+
if (crashMarker && !error) {
|
|
309
|
+
// Extract crash log and autoRollback flag from marker
|
|
310
|
+
stackTraceString = crashMarker[@"crashLog"] ?: @"";
|
|
311
|
+
// Use the autoRollback flag that was determined at crash time (previous session)
|
|
312
|
+
isAutoRollback = [crashMarker[@"isAutoRollback"] boolValue];
|
|
313
|
+
}
|
|
314
|
+
} @catch (NSException *e) {
|
|
315
|
+
// Fallback for old format (non-JSON)
|
|
316
|
+
stackTraceString = jsonContent;
|
|
317
|
+
// Default to true for old format (conservative approach)
|
|
318
|
+
isAutoRollback = YES;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (stackTraceString.length > 900) {
|
|
322
|
+
stackTraceString = [stackTraceString substringToIndex:900];
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
StallionMeta *meta = stateManager.stallionMeta;
|
|
326
|
+
SwitchState switchState = meta.switchState;
|
|
327
|
+
|
|
328
|
+
if (switchState == SwitchStateProd) {
|
|
329
|
+
NSString *currentHash = [meta getActiveReleaseHash] ?: @"";
|
|
330
|
+
// Use isAutoRollback from previous crash, not current session state
|
|
331
|
+
@try {
|
|
332
|
+
[[StallionEventHandler sharedInstance] cacheEvent:StallionObjConstants.exception_prod_event
|
|
333
|
+
eventPayload:@{
|
|
334
|
+
@"meta": stackTraceString,
|
|
335
|
+
StallionObjConstants.release_hash_key: currentHash,
|
|
336
|
+
StallionObjConstants.is_auto_rollback_key: isAutoRollback ? @"true" : @"false"
|
|
337
|
+
}];
|
|
338
|
+
} @catch (NSException *e) { }
|
|
339
|
+
|
|
340
|
+
if (isAutoRollback) {
|
|
341
|
+
// Only prevent multiple executions for auto rollback cases
|
|
342
|
+
bool expected = false;
|
|
343
|
+
if (rollbackPerformed.compare_exchange_strong(expected, true)) {
|
|
344
|
+
@try {
|
|
345
|
+
[StallionSlotManager rollbackProdWithAutoRollback:YES errorString:stackTraceString];
|
|
346
|
+
} @catch (NSException *e) { }
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
} else if (switchState == SwitchStateStage) {
|
|
350
|
+
NSString *currentStageHash = meta.stageNewHash ?: @"";
|
|
351
|
+
// Use isAutoRollback from previous crash, not current session state
|
|
352
|
+
@try {
|
|
353
|
+
[[StallionEventHandler sharedInstance] cacheEvent:StallionObjConstants.exception_stage_event
|
|
354
|
+
eventPayload:@{
|
|
355
|
+
@"meta": stackTraceString,
|
|
356
|
+
StallionObjConstants.release_hash_key: currentStageHash,
|
|
357
|
+
StallionObjConstants.is_auto_rollback_key: isAutoRollback ? @"true" : @"false"
|
|
358
|
+
}];
|
|
359
|
+
} @catch (NSException *e) { }
|
|
360
|
+
|
|
361
|
+
if (isAutoRollback) {
|
|
362
|
+
// Only prevent multiple executions for auto rollback cases
|
|
363
|
+
bool expected = false;
|
|
364
|
+
if (rollbackPerformed.compare_exchange_strong(expected, true)) {
|
|
365
|
+
@try {
|
|
366
|
+
[StallionSlotManager rollbackStage];
|
|
367
|
+
} @catch (NSException *e) { }
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Delete marker
|
|
373
|
+
[StallionFileManager deleteFileOrFolderSilently:markerPath];
|
|
374
|
+
hasProcessedNativeCrashMarker.store(true);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
} @catch (NSException *e) { }
|
|
378
|
+
}
|
|
379
|
+
|
|
@@ -17,6 +17,7 @@ class StallionFileDownloader: NSObject {
|
|
|
17
17
|
private var _reject: RCTPromiseRejectBlock?
|
|
18
18
|
private var _onProgress: ((Float) -> Void)?
|
|
19
19
|
private var lastSentProgress: Float = 0
|
|
20
|
+
private var lastProgressEmitTime: TimeInterval = 0
|
|
20
21
|
private var _downloadDirectory: String?
|
|
21
22
|
|
|
22
23
|
private let queue = DispatchQueue(label: "com.stallion.networkmanager", qos: .background)
|
|
@@ -116,13 +117,20 @@ class StallionFileDownloader: NSObject {
|
|
|
116
117
|
guard bytesRead == headerSize else { return false }
|
|
117
118
|
return header == [0x50, 0x4B, 0x03, 0x04] // PKZIP magic number
|
|
118
119
|
}
|
|
120
|
+
|
|
121
|
+
private func shouldEmitProgress(currentTime: TimeInterval) -> Bool {
|
|
122
|
+
return currentTime - self.lastProgressEmitTime >= StallionConstants.PROGRESS_THROTTLE_INTERVAL_MS
|
|
123
|
+
}
|
|
119
124
|
}
|
|
120
125
|
|
|
121
126
|
extension StallionFileDownloader: URLSessionDownloadDelegate {
|
|
122
127
|
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
|
|
123
128
|
let progress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
|
|
124
|
-
|
|
129
|
+
let currentTime = Date().timeIntervalSince1970
|
|
130
|
+
|
|
131
|
+
if shouldEmitProgress(currentTime: currentTime) {
|
|
125
132
|
self.lastSentProgress = progress
|
|
133
|
+
self.lastProgressEmitTime = currentTime
|
|
126
134
|
self._onProgress?(progress)
|
|
127
135
|
}
|
|
128
136
|
}
|
package/ios/main/StallionMeta.h
CHANGED
|
@@ -20,10 +20,21 @@
|
|
|
20
20
|
@property (nonatomic, copy) NSString *prodNewHash;
|
|
21
21
|
@property (nonatomic, copy) NSString *prodStableHash;
|
|
22
22
|
@property (nonatomic, copy) NSString *lastRolledBackHash;
|
|
23
|
+
@property (nonatomic, assign) NSTimeInterval lastRolledBackAt;
|
|
24
|
+
@property (nonatomic, assign) NSInteger successfulLaunchCount;
|
|
25
|
+
@property (nonatomic, copy) NSString *lastSuccessfulLaunchHash;
|
|
26
|
+
|
|
27
|
+
+ (NSInteger)maxSuccessLaunchThreshold;
|
|
28
|
+
+ (NSTimeInterval)lastRolledBackTTL;
|
|
23
29
|
|
|
24
30
|
- (void)reset;
|
|
25
31
|
- (NSDictionary *)toDictionary;
|
|
32
|
+
- (NSString *)getHashAtCurrentProdSlot;
|
|
26
33
|
- (NSString *)getActiveReleaseHash;
|
|
34
|
+
- (NSString *)getLastRolledBackHash;
|
|
35
|
+
- (void)setLastRolledBackHashWithTimestamp:(NSString *)lastRolledBackHash;
|
|
36
|
+
- (void)markSuccessfulLaunch:(NSString *)releaseHash;
|
|
37
|
+
- (NSInteger)getSuccessfulLaunchCount:(NSString *)releaseHash;
|
|
27
38
|
+ (instancetype)fromDictionary:(NSDictionary *)dict;
|
|
28
39
|
|
|
29
40
|
@end
|
package/ios/main/StallionMeta.m
CHANGED
|
@@ -9,6 +9,14 @@
|
|
|
9
9
|
|
|
10
10
|
@implementation StallionMeta
|
|
11
11
|
|
|
12
|
+
+ (NSInteger)maxSuccessLaunchThreshold {
|
|
13
|
+
return 3;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
+ (NSTimeInterval)lastRolledBackTTL {
|
|
17
|
+
return 6 * 60 * 60; // 6 hours in seconds
|
|
18
|
+
}
|
|
19
|
+
|
|
12
20
|
- (instancetype)init {
|
|
13
21
|
self = [super init];
|
|
14
22
|
if (self) [self reset];
|
|
@@ -26,6 +34,9 @@
|
|
|
26
34
|
self.prodNewHash = @"";
|
|
27
35
|
self.prodStableHash = @"";
|
|
28
36
|
self.lastRolledBackHash = @"";
|
|
37
|
+
self.lastRolledBackAt = 0.0;
|
|
38
|
+
self.successfulLaunchCount = 0;
|
|
39
|
+
self.lastSuccessfulLaunchHash = @"";
|
|
29
40
|
}
|
|
30
41
|
|
|
31
42
|
- (NSDictionary *)toDictionary {
|
|
@@ -43,7 +54,10 @@
|
|
|
43
54
|
@"stableHash": self.prodStableHash ?: @"",
|
|
44
55
|
@"currentSlot": [StallionMetaConstants stringFromSlotState:self.currentProdSlot] ?: @""
|
|
45
56
|
},
|
|
46
|
-
|
|
57
|
+
@"lastRolledBackHash": self.lastRolledBackHash ?: @"",
|
|
58
|
+
@"lastRolledBackAt": @(self.lastRolledBackAt),
|
|
59
|
+
@"successfulLaunchCount": @(self.successfulLaunchCount),
|
|
60
|
+
@"lastSuccessfulLaunchHash": self.lastSuccessfulLaunchHash ?: @""
|
|
47
61
|
};
|
|
48
62
|
} @catch (NSException *exception) {
|
|
49
63
|
NSLog(@"Error in toDictionary: %@", exception.reason);
|
|
@@ -87,6 +101,9 @@
|
|
|
87
101
|
meta.currentProdSlot = [StallionMetaConstants slotStateFromString:prodSlot[@"currentSlot"] ?: @"default_slot"];
|
|
88
102
|
|
|
89
103
|
meta.lastRolledBackHash = dict[@"lastRolledBackHash"] ?: @"";
|
|
104
|
+
meta.lastRolledBackAt = [dict[@"lastRolledBackAt"] doubleValue];
|
|
105
|
+
meta.successfulLaunchCount = [dict[@"successfulLaunchCount"] integerValue];
|
|
106
|
+
meta.lastSuccessfulLaunchHash = dict[@"lastSuccessfulLaunchHash"] ?: @"";
|
|
90
107
|
|
|
91
108
|
return meta;
|
|
92
109
|
} @catch (NSException *exception) {
|
|
@@ -95,4 +112,62 @@
|
|
|
95
112
|
}
|
|
96
113
|
}
|
|
97
114
|
|
|
115
|
+
- (NSString *)getHashAtCurrentProdSlot {
|
|
116
|
+
switch (self.currentProdSlot) {
|
|
117
|
+
case SlotStateNewSlot:
|
|
118
|
+
return self.prodNewHash;
|
|
119
|
+
case SlotStateStableSlot:
|
|
120
|
+
return self.prodStableHash;
|
|
121
|
+
default:
|
|
122
|
+
return @"";
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
- (NSString *)getLastRolledBackHash {
|
|
127
|
+
[self enforceLastRolledBackExpiry];
|
|
128
|
+
return self.lastRolledBackHash;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
- (void)setLastRolledBackHashWithTimestamp:(NSString *)lastRolledBackHash {
|
|
132
|
+
NSString *hashValue = lastRolledBackHash ?: @"";
|
|
133
|
+
self.lastRolledBackHash = hashValue;
|
|
134
|
+
self.lastRolledBackAt = [hashValue isEqualToString:@""] ? 0.0 : [[NSDate date] timeIntervalSince1970];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
- (void)enforceLastRolledBackExpiry {
|
|
138
|
+
if (!self.lastRolledBackHash || [self.lastRolledBackHash isEqualToString:@""]) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (self.lastRolledBackAt <= 0.0) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
NSTimeInterval now = [[NSDate date] timeIntervalSince1970];
|
|
145
|
+
if (now - self.lastRolledBackAt >= [StallionMeta lastRolledBackTTL]) {
|
|
146
|
+
self.lastRolledBackHash = @"";
|
|
147
|
+
self.lastRolledBackAt = 0.0;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
- (void)markSuccessfulLaunch:(NSString *)releaseHash {
|
|
152
|
+
if (!releaseHash || [releaseHash isEqualToString:@""]) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (![releaseHash isEqualToString:self.lastSuccessfulLaunchHash]) {
|
|
156
|
+
self.successfulLaunchCount = 0;
|
|
157
|
+
self.lastSuccessfulLaunchHash = releaseHash;
|
|
158
|
+
}
|
|
159
|
+
if (self.successfulLaunchCount < 3) { // MAX_SUCCESS_LAUNCH_THRESHOLD
|
|
160
|
+
self.successfulLaunchCount += 1;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
- (NSInteger)getSuccessfulLaunchCount:(NSString *)releaseHash {
|
|
165
|
+
NSString *currentHash = releaseHash ?: @"";
|
|
166
|
+
if (![currentHash isEqualToString:self.lastSuccessfulLaunchHash]) {
|
|
167
|
+
return 0;
|
|
168
|
+
} else {
|
|
169
|
+
return self.successfulLaunchCount;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
98
173
|
@end
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
NSString *newSlotPath = [NSString stringWithFormat:@"%@/%@/%@", baseFolderPath, StallionObjConstants.prod_directory, StallionObjConstants.new_folder_slot];
|
|
75
75
|
NSString *stableSlotPath = [NSString stringWithFormat:@"%@/%@/%@", baseFolderPath, StallionObjConstants.prod_directory, StallionObjConstants.stable_folder_slot];
|
|
76
76
|
|
|
77
|
-
[StallionFileManager
|
|
77
|
+
[StallionFileManager moveFileFrom:newSlotPath to:stableSlotPath];
|
|
78
78
|
|
|
79
79
|
NSString *newReleaseHash = stateManager.stallionMeta.prodNewHash;
|
|
80
80
|
[stateManager.stallionMeta setProdStableHash:newReleaseHash];
|
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
|
|
97
97
|
if (isAutoRollback) {
|
|
98
98
|
StallionStateManager *stateManager = [StallionStateManager sharedInstance];
|
|
99
|
-
[stateManager.stallionMeta
|
|
99
|
+
[stateManager.stallionMeta setLastRolledBackHashWithTimestamp:rolledBackReleaseHash];
|
|
100
100
|
[stateManager syncStallionMeta];
|
|
101
101
|
}
|
|
102
102
|
|
|
@@ -44,6 +44,9 @@ static StallionStateManager *_instance = nil;
|
|
|
44
44
|
_isMounted = NO;
|
|
45
45
|
_pendingReleaseUrl = @"";
|
|
46
46
|
_pendingReleaseHash = @"";
|
|
47
|
+
|
|
48
|
+
// Reset mount state on initialization (ensures mount marker file is deleted for new session)
|
|
49
|
+
[self setIsMounted:NO];
|
|
47
50
|
}
|
|
48
51
|
return self;
|
|
49
52
|
}
|
|
@@ -70,6 +73,32 @@ static StallionStateManager *_instance = nil;
|
|
|
70
73
|
[self syncStallionMeta];
|
|
71
74
|
}
|
|
72
75
|
|
|
76
|
+
- (void)setIsMounted:(BOOL)isMounted {
|
|
77
|
+
_isMounted = isMounted;
|
|
78
|
+
// Write mount state to a simple file that signal handler can read (async-signal-safe)
|
|
79
|
+
NSString *filesDir = self.stallionConfig.filesDirectory;
|
|
80
|
+
NSString *mountMarkerPath = [NSString stringWithFormat:@"%@/stallion_mount.marker", filesDir];
|
|
81
|
+
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
82
|
+
|
|
83
|
+
if (isMounted) {
|
|
84
|
+
// Create file to indicate mounted (file existence = mounted)
|
|
85
|
+
@try {
|
|
86
|
+
[fileManager createFileAtPath:mountMarkerPath contents:nil attributes:nil];
|
|
87
|
+
} @catch (NSException *e) {
|
|
88
|
+
// Silently ignore errors
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
// Delete file to indicate not mounted (no file = not mounted)
|
|
92
|
+
@try {
|
|
93
|
+
if ([fileManager fileExistsAtPath:mountMarkerPath]) {
|
|
94
|
+
[fileManager removeItemAtPath:mountMarkerPath error:nil];
|
|
95
|
+
}
|
|
96
|
+
} @catch (NSException *e) {
|
|
97
|
+
// Silently ignore errors
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
73
102
|
#pragma mark - Local Storage Methods
|
|
74
103
|
|
|
75
104
|
- (NSString *)getStringForKey:(NSString *)key defaultValue:(NSString *)defaultValue {
|
|
@@ -46,6 +46,7 @@ class StallionSyncHandler {
|
|
|
46
46
|
"platform": StallionConstants.PlatformValue,
|
|
47
47
|
"projectId": projectId,
|
|
48
48
|
"appliedBundleHash": appliedBundleHash,
|
|
49
|
+
"deviceMeta": StallionDeviceInfo.getDeviceMetaJson(config)
|
|
49
50
|
]
|
|
50
51
|
|
|
51
52
|
// Make API call using URLSession
|
|
@@ -151,7 +152,7 @@ class StallionSyncHandler {
|
|
|
151
152
|
!newReleaseHash.isEmpty else { return }
|
|
152
153
|
|
|
153
154
|
let stateManager = StallionStateManager.sharedInstance()
|
|
154
|
-
let lastRolledBackHash = stateManager?.stallionMeta?.
|
|
155
|
+
let lastRolledBackHash = stateManager?.stallionMeta?.getLastRolledBackHash() ?? ""
|
|
155
156
|
let lastUnverifiedHash = stateManager?.stallionConfig?.lastUnverifiedHash ?? ""
|
|
156
157
|
|
|
157
158
|
if newReleaseHash != lastRolledBackHash && newReleaseHash != lastUnverifiedHash {
|
|
@@ -190,7 +191,7 @@ class StallionSyncHandler {
|
|
|
190
191
|
url: fromUrl,
|
|
191
192
|
downloadDirectory: downloadPath,
|
|
192
193
|
onProgress: { progress in
|
|
193
|
-
|
|
194
|
+
emitDownloadProgress(releaseHash: newReleaseHash, progress: progress)
|
|
194
195
|
},
|
|
195
196
|
resolve: { _ in
|
|
196
197
|
completeDownload()
|
|
@@ -278,5 +279,16 @@ class StallionSyncHandler {
|
|
|
278
279
|
shouldCache: true
|
|
279
280
|
)
|
|
280
281
|
}
|
|
282
|
+
|
|
283
|
+
private static func emitDownloadProgress(releaseHash: String, progress: Float) {
|
|
284
|
+
let progressPayload: NSDictionary = [
|
|
285
|
+
"releaseHash": releaseHash,
|
|
286
|
+
"progress": "\(progress)"
|
|
287
|
+
]
|
|
288
|
+
Stallion.sendEventToRn(eventName: StallionConstants.NativeEventTypesProd.DOWNLOAD_PROGRESS_PROD,
|
|
289
|
+
eventBody: progressPayload,
|
|
290
|
+
shouldCache: false
|
|
291
|
+
)
|
|
292
|
+
}
|
|
281
293
|
}
|
|
282
294
|
|
package/package.json
CHANGED
|
@@ -3,6 +3,28 @@ require "json"
|
|
|
3
3
|
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
4
4
|
folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32'
|
|
5
5
|
|
|
6
|
+
# Generate StallionVersion.h during pod install
|
|
7
|
+
version_header_path = File.join(__dir__, "ios/main/StallionVersion.h")
|
|
8
|
+
version_header_content = <<~HEADER
|
|
9
|
+
//
|
|
10
|
+
// StallionVersion.h
|
|
11
|
+
// react-native-stallion
|
|
12
|
+
//
|
|
13
|
+
// Auto-generated from package.json during pod install
|
|
14
|
+
// Do not edit this file manually
|
|
15
|
+
//
|
|
16
|
+
|
|
17
|
+
#ifndef StallionVersion_h
|
|
18
|
+
#define StallionVersion_h
|
|
19
|
+
|
|
20
|
+
#define STALLION_SDK_VERSION @"#{package["version"]}"
|
|
21
|
+
|
|
22
|
+
#endif /* StallionVersion_h */
|
|
23
|
+
HEADER
|
|
24
|
+
|
|
25
|
+
File.write(version_header_path, version_header_content)
|
|
26
|
+
puts "Stallion: Generated StallionVersion.h with version #{package["version"]}"
|
|
27
|
+
|
|
6
28
|
Pod::Spec.new do |s|
|
|
7
29
|
s.name = "react-native-stallion"
|
|
8
30
|
s.version = package["version"]
|