react-native-update 10.51.0 → 10.52.0
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/NATIVE_CHECKUPDATE_DESIGN.md +108 -0
- package/NATIVE_CHECK_FOLLOWUPS.md +46 -0
- package/README-CN.md +2 -0
- package/README.md +2 -0
- package/android/build.gradle +10 -0
- package/android/src/main/java/cn/reactnative/modules/update/CrashRescue.java +115 -0
- package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +259 -26
- package/android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java +209 -19
- package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +6 -0
- package/harmony/pushy/src/main/ets/DownloadTask.ts +381 -72
- package/harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts +32 -2
- package/harmony/pushy/src/main/ets/UpdateContext.ts +5 -0
- package/harmony/pushy.har +0 -0
- package/ios/RCTPushy/RCTPushy.mm +308 -25
- package/ios/RCTPushy/RCTPushyDownloader.mm +361 -74
- package/package.json +1 -1
- package/src/client.ts +8 -0
- package/src/type.ts +6 -0
|
@@ -2,14 +2,94 @@
|
|
|
2
2
|
|
|
3
3
|
static NSString *const RCTPushyDownloaderErrorDomain = @"cn.reactnative.pushy";
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
// Cross-launch resumable download (NATIVE_CHECKUPDATE_DESIGN §11.4): the
|
|
6
|
+
// archive streams straight into savePath (an NSURLSessionDownloadTask's
|
|
7
|
+
// temporary file is discarded on failure, which made every partial byte
|
|
8
|
+
// worthless), and a sidecar next to it records what the partial belongs to
|
|
9
|
+
// (url + validators + total). A brick gets a few hundred milliseconds per
|
|
10
|
+
// launch plus a bounded crash-rescue window, so progress must be monotonic
|
|
11
|
+
// across process deaths.
|
|
12
|
+
|
|
13
|
+
static NSString *RCTPushyResumeSidecarPath(NSString *savePath) {
|
|
14
|
+
return [savePath stringByAppendingString:@".resume"];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
static NSDictionary *RCTPushyReadResumeMeta(NSString *savePath, NSString *url) {
|
|
18
|
+
NSData *data = [NSData dataWithContentsOfFile:RCTPushyResumeSidecarPath(savePath)];
|
|
19
|
+
if (data == nil) {
|
|
20
|
+
return nil;
|
|
21
|
+
}
|
|
22
|
+
id meta = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
|
|
23
|
+
if (![meta isKindOfClass:[NSDictionary class]]) {
|
|
24
|
+
return nil;
|
|
25
|
+
}
|
|
26
|
+
// An archive whose sidecar names another URL is untrusted.
|
|
27
|
+
if (![url isEqualToString:meta[@"url"]]) {
|
|
28
|
+
return nil;
|
|
29
|
+
}
|
|
30
|
+
return meta;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
static void RCTPushyDeleteResumeSidecar(NSString *savePath) {
|
|
34
|
+
[[NSFileManager defaultManager] removeItemAtPath:RCTPushyResumeSidecarPath(savePath)
|
|
35
|
+
error:nil];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
static long long RCTPushyFileSize(NSString *path) {
|
|
39
|
+
NSDictionary *attributes =
|
|
40
|
+
[[NSFileManager defaultManager] attributesOfItemAtPath:path error:nil];
|
|
41
|
+
NSNumber *size = attributes[NSFileSize];
|
|
42
|
+
return size == nil ? -1 : size.longLongValue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// "bytes <start>-<end>/<total>". Returns the total (0 when "*"), or -1 when
|
|
46
|
+
// missing/malformed or the start does not match the local partial.
|
|
47
|
+
static long long RCTPushyParseContentRange(NSString *header, long long expectedStart) {
|
|
48
|
+
if (![header hasPrefix:@"bytes "]) {
|
|
49
|
+
return -1;
|
|
50
|
+
}
|
|
51
|
+
NSString *range = [[header substringFromIndex:6]
|
|
52
|
+
stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
|
|
53
|
+
NSRange slash = [range rangeOfString:@"/"];
|
|
54
|
+
NSRange dash = [range rangeOfString:@"-"];
|
|
55
|
+
if (slash.location == NSNotFound || dash.location == NSNotFound
|
|
56
|
+
|| dash.location > slash.location) {
|
|
57
|
+
return -1;
|
|
58
|
+
}
|
|
59
|
+
long long start = [range substringToIndex:dash.location].longLongValue;
|
|
60
|
+
if (start != expectedStart) {
|
|
61
|
+
return -1;
|
|
62
|
+
}
|
|
63
|
+
NSString *totalPart = [range substringFromIndex:slash.location + 1];
|
|
64
|
+
if ([totalPart isEqualToString:@"*"]) {
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
long long total = totalPart.longLongValue;
|
|
68
|
+
return total > 0 ? total : -1;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@interface RCTPushyDownloader()<NSURLSessionDataDelegate>
|
|
6
72
|
|
|
7
73
|
@property (nonatomic, strong) NSURLSession *session;
|
|
8
74
|
@property (copy) void (^progressHandler)(long long, long long);
|
|
9
75
|
@property (copy) void (^completionHandler)(NSString*, NSError*);
|
|
10
76
|
@property (copy) NSString *savePath;
|
|
77
|
+
@property (copy) NSString *urlString;
|
|
78
|
+
@property (nonatomic, assign) NSTimeInterval timeoutInterval;
|
|
79
|
+
@property (nonatomic, strong) NSFileHandle *fileHandle;
|
|
80
|
+
@property (nonatomic, strong) NSDictionary *resumeMeta;
|
|
81
|
+
@property (nonatomic, assign) long long resumeOffset; // requested Range start
|
|
82
|
+
@property (nonatomic, assign) long long baseOffset; // granted by the response
|
|
83
|
+
@property (nonatomic, assign) long long receivedBytes; // streamed this session
|
|
84
|
+
@property (nonatomic, assign) long long contentLength; // this response's body
|
|
85
|
+
@property (nonatomic, assign) long long expectedTotal; // whole file (0 unknown)
|
|
11
86
|
@property (nonatomic, strong) NSError *fileError;
|
|
12
87
|
@property (nonatomic, assign) BOOL finished;
|
|
88
|
+
@property (nonatomic, assign) BOOL retriedFromZero;
|
|
89
|
+
// The server encoded the body itself (Content-Encoding other than identity):
|
|
90
|
+
// NSURLSession delivers decoded bytes, so length accounting against the
|
|
91
|
+
// encoded Content-Length is meaningless and resume offsets cannot be trusted.
|
|
92
|
+
@property (nonatomic, assign) BOOL encodedBody;
|
|
13
93
|
@property (nonatomic, assign) int lastReportedPercentage;
|
|
14
94
|
@property (nonatomic, assign) long long lastReportedBytes;
|
|
15
95
|
@end
|
|
@@ -28,13 +108,15 @@ completionHandler:(void (^)(NSString *path, NSError *error))completionHandler
|
|
|
28
108
|
downloader.progressHandler = progressHandler;
|
|
29
109
|
downloader.completionHandler = completionHandler;
|
|
30
110
|
downloader.savePath = savePath;
|
|
111
|
+
downloader.urlString = downloadPath;
|
|
112
|
+
downloader.timeoutInterval = timeoutInterval;
|
|
31
113
|
|
|
32
|
-
[downloader
|
|
114
|
+
[downloader startTransfer];
|
|
33
115
|
}
|
|
34
116
|
|
|
35
|
-
- (void)
|
|
117
|
+
- (void)startTransfer
|
|
36
118
|
{
|
|
37
|
-
NSURL *url = [NSURL URLWithString:
|
|
119
|
+
NSURL *url = [NSURL URLWithString:self.urlString];
|
|
38
120
|
if (url == nil) {
|
|
39
121
|
[self completeWithError:[NSError errorWithDomain:RCTPushyDownloaderErrorDomain
|
|
40
122
|
code:-1
|
|
@@ -44,6 +126,47 @@ completionHandler:(void (^)(NSString *path, NSError *error))completionHandler
|
|
|
44
126
|
return;
|
|
45
127
|
}
|
|
46
128
|
|
|
129
|
+
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
130
|
+
self.resumeMeta = RCTPushyReadResumeMeta(self.savePath, self.urlString);
|
|
131
|
+
self.resumeOffset = 0;
|
|
132
|
+
if (self.resumeMeta != nil) {
|
|
133
|
+
long long size = RCTPushyFileSize(self.savePath);
|
|
134
|
+
long long knownTotal = [self.resumeMeta[@"total"] longLongValue];
|
|
135
|
+
if (size > 0 && knownTotal > 0 && size == knownTotal) {
|
|
136
|
+
// Fully received in a previous attempt (the process died between
|
|
137
|
+
// download end and unzip): nothing left to transfer.
|
|
138
|
+
if (self.progressHandler) {
|
|
139
|
+
self.progressHandler(knownTotal, knownTotal);
|
|
140
|
+
}
|
|
141
|
+
[self completeWithError:nil];
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (size > 0 && (knownTotal <= 0 || size < knownTotal)) {
|
|
145
|
+
self.resumeOffset = size;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (self.resumeOffset == 0) {
|
|
149
|
+
[fileManager removeItemAtPath:self.savePath error:nil];
|
|
150
|
+
RCTPushyDeleteResumeSidecar(self.savePath);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
|
|
154
|
+
if (self.resumeOffset > 0) {
|
|
155
|
+
// Only resume requests pin the encoding: Range offsets must address
|
|
156
|
+
// the same bytes that are on disk. Fresh downloads keep the system's
|
|
157
|
+
// transparent gzip handling, matching the pre-resume behaviour for
|
|
158
|
+
// servers that compress regardless.
|
|
159
|
+
[request setValue:@"identity" forHTTPHeaderField:@"Accept-Encoding"];
|
|
160
|
+
[request setValue:[NSString stringWithFormat:@"bytes=%lld-", self.resumeOffset]
|
|
161
|
+
forHTTPHeaderField:@"Range"];
|
|
162
|
+
NSString *validator = self.resumeMeta[@"etag"] ?: self.resumeMeta[@"lastModified"];
|
|
163
|
+
if (validator != nil) {
|
|
164
|
+
// With a validator the server falls back to a full 200 when the
|
|
165
|
+
// file changed instead of appending mismatched bytes.
|
|
166
|
+
[request setValue:validator forHTTPHeaderField:@"If-Range"];
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
47
170
|
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
|
|
48
171
|
// Avoid hanging forever on a stalled connection (default resource timeout
|
|
49
172
|
// is 7 days). The 30s idle timeout matches Android's readTimeout and is
|
|
@@ -51,15 +174,39 @@ completionHandler:(void (^)(NSString *path, NSError *error))completionHandler
|
|
|
51
174
|
// Android's 10min callTimeout — 300s made a 30MB full package on a slow
|
|
52
175
|
// (<100KB/s) network fail on iOS while succeeding on Android.
|
|
53
176
|
sessionConfig.timeoutIntervalForRequest = 30;
|
|
54
|
-
sessionConfig.timeoutIntervalForResource = MAX(1, timeoutInterval);
|
|
177
|
+
sessionConfig.timeoutIntervalForResource = MAX(1, self.timeoutInterval);
|
|
55
178
|
self.session = [NSURLSession sessionWithConfiguration:sessionConfig
|
|
56
179
|
delegate:self
|
|
57
180
|
delegateQueue:nil];
|
|
58
181
|
|
|
59
|
-
|
|
182
|
+
NSURLSessionDataTask *task = [self.session dataTaskWithRequest:request];
|
|
60
183
|
[task resume];
|
|
61
184
|
}
|
|
62
185
|
|
|
186
|
+
- (void)writeResumeMetaWithResponse:(NSHTTPURLResponse *)response total:(long long)total
|
|
187
|
+
{
|
|
188
|
+
NSMutableDictionary *meta = [NSMutableDictionary dictionary];
|
|
189
|
+
meta[@"url"] = self.urlString;
|
|
190
|
+
NSString *etag = response.allHeaderFields[@"ETag"] ?: self.resumeMeta[@"etag"];
|
|
191
|
+
NSString *lastModified =
|
|
192
|
+
response.allHeaderFields[@"Last-Modified"] ?: self.resumeMeta[@"lastModified"];
|
|
193
|
+
if (etag != nil) {
|
|
194
|
+
meta[@"etag"] = etag;
|
|
195
|
+
}
|
|
196
|
+
if (lastModified != nil) {
|
|
197
|
+
meta[@"lastModified"] = lastModified;
|
|
198
|
+
}
|
|
199
|
+
if (total > 0) {
|
|
200
|
+
meta[@"total"] = @(total);
|
|
201
|
+
}
|
|
202
|
+
NSData *data = [NSJSONSerialization dataWithJSONObject:meta options:0 error:nil];
|
|
203
|
+
if (data != nil) {
|
|
204
|
+
// Non-fatal on failure: without a sidecar the next attempt starts
|
|
205
|
+
// from zero.
|
|
206
|
+
[data writeToFile:RCTPushyResumeSidecarPath(self.savePath) atomically:YES];
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
63
210
|
- (void)completeWithError:(NSError *)error
|
|
64
211
|
{
|
|
65
212
|
if (self.finished) {
|
|
@@ -67,6 +214,14 @@ completionHandler:(void (^)(NSString *path, NSError *error))completionHandler
|
|
|
67
214
|
}
|
|
68
215
|
self.finished = YES;
|
|
69
216
|
|
|
217
|
+
if (self.fileHandle != nil) {
|
|
218
|
+
@try {
|
|
219
|
+
[self.fileHandle closeFile];
|
|
220
|
+
} @catch (NSException *exception) {
|
|
221
|
+
}
|
|
222
|
+
self.fileHandle = nil;
|
|
223
|
+
}
|
|
224
|
+
|
|
70
225
|
void (^completionHandler)(NSString *, NSError *) = self.completionHandler;
|
|
71
226
|
self.progressHandler = nil;
|
|
72
227
|
self.completionHandler = nil;
|
|
@@ -80,97 +235,229 @@ completionHandler:(void (^)(NSString *path, NSError *error))completionHandler
|
|
|
80
235
|
}
|
|
81
236
|
}
|
|
82
237
|
|
|
238
|
+
- (void)failWithDescription:(NSString *)description code:(NSInteger)code
|
|
239
|
+
{
|
|
240
|
+
self.fileError = [NSError errorWithDomain:RCTPushyDownloaderErrorDomain
|
|
241
|
+
code:code
|
|
242
|
+
userInfo:@{NSLocalizedDescriptionKey: description}];
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Drops the untrusted partial and reissues the whole request from zero, at
|
|
246
|
+
// most once per download (the stale-partial 416 case). Delegate callbacks
|
|
247
|
+
// from the superseded session are ignored via the session-identity guards.
|
|
248
|
+
- (void)restartFromZero
|
|
249
|
+
{
|
|
250
|
+
self.retriedFromZero = YES;
|
|
251
|
+
self.resumeMeta = nil;
|
|
252
|
+
self.resumeOffset = 0;
|
|
253
|
+
self.baseOffset = 0;
|
|
254
|
+
self.receivedBytes = 0;
|
|
255
|
+
self.contentLength = 0;
|
|
256
|
+
self.expectedTotal = 0;
|
|
257
|
+
self.encodedBody = NO;
|
|
258
|
+
self.lastReportedPercentage = 0;
|
|
259
|
+
self.lastReportedBytes = 0;
|
|
260
|
+
if (self.fileHandle != nil) {
|
|
261
|
+
@try {
|
|
262
|
+
[self.fileHandle closeFile];
|
|
263
|
+
} @catch (NSException *exception) {
|
|
264
|
+
}
|
|
265
|
+
self.fileHandle = nil;
|
|
266
|
+
}
|
|
267
|
+
[[NSFileManager defaultManager] removeItemAtPath:self.savePath error:nil];
|
|
268
|
+
RCTPushyDeleteResumeSidecar(self.savePath);
|
|
269
|
+
[self.session finishTasksAndInvalidate];
|
|
270
|
+
self.session = nil;
|
|
271
|
+
[self startTransfer];
|
|
272
|
+
}
|
|
273
|
+
|
|
83
274
|
#pragma mark - session delegate
|
|
84
275
|
|
|
85
|
-
- (void)URLSession:(NSURLSession *)session
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
|
|
276
|
+
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
|
|
277
|
+
didReceiveResponse:(NSURLResponse *)response
|
|
278
|
+
completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler
|
|
89
279
|
{
|
|
90
|
-
if (
|
|
280
|
+
if (session != self.session) {
|
|
281
|
+
// A superseded session (restartFromZero) still delivering events.
|
|
282
|
+
completionHandler(NSURLSessionResponseCancel);
|
|
91
283
|
return;
|
|
92
284
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
285
|
+
NSHTTPURLResponse *httpResponse =
|
|
286
|
+
[response isKindOfClass:[NSHTTPURLResponse class]] ? (NSHTTPURLResponse *)response : nil;
|
|
287
|
+
NSInteger statusCode = httpResponse.statusCode;
|
|
288
|
+
|
|
289
|
+
if (statusCode == 416) {
|
|
290
|
+
long long knownTotal = [self.resumeMeta[@"total"] longLongValue];
|
|
291
|
+
completionHandler(NSURLSessionResponseCancel);
|
|
292
|
+
if (knownTotal > 0 && RCTPushyFileSize(self.savePath) == knownTotal) {
|
|
293
|
+
// The partial is actually the complete file.
|
|
294
|
+
[self completeWithError:nil];
|
|
295
|
+
} else if (!self.retriedFromZero) {
|
|
296
|
+
[self restartFromZero];
|
|
297
|
+
} else {
|
|
298
|
+
[self failWithDescription:@"server rejected the download range" code:statusCode];
|
|
299
|
+
[self completeWithError:self.fileError];
|
|
300
|
+
}
|
|
301
|
+
return;
|
|
101
302
|
}
|
|
102
|
-
if (
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
303
|
+
if (httpResponse != nil && (statusCode < 200 || statusCode >= 300)) {
|
|
304
|
+
[self failWithDescription:[NSString stringWithFormat:@"unexpected http status code %ld",
|
|
305
|
+
(long)statusCode]
|
|
306
|
+
code:statusCode];
|
|
307
|
+
completionHandler(NSURLSessionResponseCancel);
|
|
308
|
+
[self completeWithError:self.fileError];
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
NSString *contentEncoding =
|
|
313
|
+
[httpResponse.allHeaderFields[@"Content-Encoding"] lowercaseString];
|
|
314
|
+
self.encodedBody =
|
|
315
|
+
contentEncoding.length > 0 && ![contentEncoding isEqualToString:@"identity"];
|
|
316
|
+
|
|
317
|
+
BOOL append = statusCode == 206 && self.resumeOffset > 0;
|
|
318
|
+
if (append && (self.encodedBody
|
|
319
|
+
|| RCTPushyParseContentRange(httpResponse.allHeaderFields[@"Content-Range"],
|
|
320
|
+
self.resumeOffset) < 0)) {
|
|
321
|
+
// Encoded range bytes or a malformed/mismatched Content-Range: the
|
|
322
|
+
// appended bytes could not be trusted. Treat like a stale partial —
|
|
323
|
+
// one clean retry from zero — instead of failing terminally, which
|
|
324
|
+
// would keep the partial and hit the same wall on every attempt.
|
|
325
|
+
completionHandler(NSURLSessionResponseCancel);
|
|
326
|
+
if (!self.retriedFromZero) {
|
|
327
|
+
[self restartFromZero];
|
|
328
|
+
} else {
|
|
329
|
+
[self failWithDescription:@"untrusted resume response" code:-1];
|
|
330
|
+
[self completeWithError:self.fileError];
|
|
106
331
|
}
|
|
107
|
-
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
self.baseOffset = append ? self.resumeOffset : 0;
|
|
335
|
+
self.contentLength = response.expectedContentLength;
|
|
336
|
+
if (append) {
|
|
337
|
+
self.expectedTotal = RCTPushyParseContentRange(
|
|
338
|
+
httpResponse.allHeaderFields[@"Content-Range"], self.resumeOffset);
|
|
339
|
+
} else if (self.encodedBody) {
|
|
340
|
+
// Decoded bytes are being written; the encoded lengths say nothing.
|
|
341
|
+
self.expectedTotal = 0;
|
|
108
342
|
} else {
|
|
109
|
-
|
|
110
|
-
|
|
343
|
+
self.expectedTotal = self.contentLength > 0 ? self.contentLength : 0;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
347
|
+
if (!append) {
|
|
348
|
+
// The server ignored the range (or none was sent): start over.
|
|
349
|
+
[fileManager removeItemAtPath:self.savePath error:nil];
|
|
350
|
+
[fileManager createFileAtPath:self.savePath contents:nil attributes:nil];
|
|
351
|
+
}
|
|
352
|
+
self.fileHandle = [NSFileHandle fileHandleForWritingAtPath:self.savePath];
|
|
353
|
+
if (self.fileHandle == nil) {
|
|
354
|
+
[self failWithDescription:@"cannot open download file for writing" code:-1];
|
|
355
|
+
completionHandler(NSURLSessionResponseCancel);
|
|
356
|
+
[self completeWithError:self.fileError];
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (append) {
|
|
360
|
+
@try {
|
|
361
|
+
[self.fileHandle seekToEndOfFile];
|
|
362
|
+
} @catch (NSException *exception) {
|
|
363
|
+
[self failWithDescription:@"cannot seek download file" code:-1];
|
|
364
|
+
completionHandler(NSURLSessionResponseCancel);
|
|
365
|
+
[self completeWithError:self.fileError];
|
|
111
366
|
return;
|
|
112
367
|
}
|
|
113
|
-
self.lastReportedBytes = totalBytesWritten;
|
|
114
368
|
}
|
|
115
|
-
self.
|
|
369
|
+
if (self.encodedBody) {
|
|
370
|
+
// No resume across an encoded transfer: on-disk bytes are decoded,
|
|
371
|
+
// Range offsets would address the encoded representation.
|
|
372
|
+
RCTPushyDeleteResumeSidecar(self.savePath);
|
|
373
|
+
} else if (httpResponse != nil) {
|
|
374
|
+
// Persist before streaming so a mid-stream crash can resume.
|
|
375
|
+
[self writeResumeMetaWithResponse:httpResponse total:self.expectedTotal];
|
|
376
|
+
}
|
|
377
|
+
completionHandler(NSURLSessionResponseAllow);
|
|
116
378
|
}
|
|
117
379
|
|
|
118
|
-
- (void)URLSession:(NSURLSession *)session
|
|
119
|
-
|
|
380
|
+
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
|
|
381
|
+
didReceiveData:(NSData *)data
|
|
120
382
|
{
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
383
|
+
if (session != self.session) {
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
if (self.fileHandle == nil || self.fileError != nil) {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
@try {
|
|
390
|
+
[self.fileHandle writeData:data];
|
|
391
|
+
} @catch (NSException *exception) {
|
|
392
|
+
[self failWithDescription:[NSString stringWithFormat:@"write failed: %@",
|
|
393
|
+
exception.reason]
|
|
394
|
+
code:-1];
|
|
395
|
+
[dataTask cancel];
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
self.receivedBytes += data.length;
|
|
399
|
+
|
|
400
|
+
if (!self.progressHandler) {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
long long overall = self.baseOffset + self.receivedBytes;
|
|
404
|
+
long long total = self.expectedTotal;
|
|
405
|
+
if (overall > total) {
|
|
406
|
+
// Should not happen with identity encoding; treat as unknown so the
|
|
407
|
+
// JS side never sees percentages past 100.
|
|
408
|
+
total = 0;
|
|
409
|
+
}
|
|
410
|
+
if (total > 0) {
|
|
411
|
+
int percentage = (int)((overall * 100.0 / total) + 0.5);
|
|
412
|
+
if (percentage <= self.lastReportedPercentage) {
|
|
134
413
|
return;
|
|
135
414
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
//
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
NSString *contentEncoding = [httpResponse.allHeaderFields[@"Content-Encoding"] lowercaseString];
|
|
142
|
-
BOOL isEncodedBody = contentEncoding.length > 0 && ![contentEncoding isEqualToString:@"identity"];
|
|
143
|
-
long long expectedLength = httpResponse.expectedContentLength;
|
|
144
|
-
if (!isEncodedBody && expectedLength > 0) {
|
|
145
|
-
NSNumber *fileSize = [[NSFileManager defaultManager] attributesOfItemAtPath:location.path
|
|
146
|
-
error:nil][NSFileSize];
|
|
147
|
-
if (fileSize != nil && fileSize.longLongValue != expectedLength) {
|
|
148
|
-
self.fileError = [NSError errorWithDomain:RCTPushyDownloaderErrorDomain
|
|
149
|
-
code:-1
|
|
150
|
-
userInfo:@{
|
|
151
|
-
NSLocalizedDescriptionKey: [NSString stringWithFormat:@"download incomplete: expected %lld bytes, got %lld", expectedLength, fileSize.longLongValue],
|
|
152
|
-
}];
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
415
|
+
self.lastReportedPercentage = percentage;
|
|
416
|
+
} else {
|
|
417
|
+
// Total unknown: throttle by bytes to avoid flooding the bridge.
|
|
418
|
+
if (overall - self.lastReportedBytes < 256 * 1024) {
|
|
419
|
+
return;
|
|
155
420
|
}
|
|
421
|
+
self.lastReportedBytes = overall;
|
|
156
422
|
}
|
|
157
|
-
|
|
158
|
-
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
159
|
-
[fileManager removeItemAtPath:self.savePath error:nil];
|
|
160
|
-
|
|
161
|
-
NSError *fileError = nil;
|
|
162
|
-
[fileManager moveItemAtURL:location
|
|
163
|
-
toURL:[NSURL fileURLWithPath:self.savePath]
|
|
164
|
-
error:&fileError];
|
|
165
|
-
if (fileError != nil) {
|
|
166
|
-
self.fileError = fileError;
|
|
167
|
-
}
|
|
423
|
+
self.progressHandler(overall, total);
|
|
168
424
|
}
|
|
169
425
|
|
|
170
426
|
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
|
|
171
427
|
didCompleteWithError:(NSError *)error
|
|
172
428
|
{
|
|
173
|
-
|
|
429
|
+
if (session != self.session) {
|
|
430
|
+
// A superseded session (restartFromZero) settling its cancelled
|
|
431
|
+
// task; only the current session may decide this download's fate.
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
if (self.finished) {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
// The locally recorded failure (e.g. a write error that cancelled the
|
|
438
|
+
// task) is the actual cause; the session error would just say
|
|
439
|
+
// "cancelled".
|
|
440
|
+
NSError *finalError = self.fileError ?: error;
|
|
441
|
+
if (finalError == nil && !self.encodedBody) {
|
|
442
|
+
// Reject truncated transfers like Android/Harmony do. Skipped for
|
|
443
|
+
// encoded bodies: the on-disk size is decoded bytes while the
|
|
444
|
+
// expected lengths count encoded ones.
|
|
445
|
+
if (self.contentLength >= 0 && self.receivedBytes != self.contentLength) {
|
|
446
|
+
[self failWithDescription:[NSString stringWithFormat:
|
|
447
|
+
@"download incomplete: expected %lld bytes, got %lld",
|
|
448
|
+
self.contentLength, self.receivedBytes] code:-1];
|
|
449
|
+
finalError = self.fileError;
|
|
450
|
+
} else if (self.expectedTotal > 0
|
|
451
|
+
&& RCTPushyFileSize(self.savePath) != self.expectedTotal) {
|
|
452
|
+
[self failWithDescription:[NSString stringWithFormat:
|
|
453
|
+
@"download incomplete: expected %lld total bytes, got %lld",
|
|
454
|
+
self.expectedTotal, RCTPushyFileSize(self.savePath)] code:-1];
|
|
455
|
+
finalError = self.fileError;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
// On failure the partial + sidecar stay on disk — that is the resume
|
|
459
|
+
// state a later launch (or crash-rescue window) picks up.
|
|
460
|
+
[self completeWithError:finalError];
|
|
174
461
|
}
|
|
175
462
|
|
|
176
463
|
@end
|
package/package.json
CHANGED
package/src/client.ts
CHANGED
|
@@ -838,6 +838,14 @@ export class Pushy {
|
|
|
838
838
|
}
|
|
839
839
|
sharedState.marked = true;
|
|
840
840
|
this.report({ type: 'markSuccess' });
|
|
841
|
+
// 救砖回执:这个版本是被救援通道送进来的,且活过了健康确认——遥测里
|
|
842
|
+
// 一条 rescue 回执就是"这台设备被捞回来了"的证据。
|
|
843
|
+
if (currentVersionInfo.forceBootRescue) {
|
|
844
|
+
this.report({ type: 'forceBootRescue' });
|
|
845
|
+
}
|
|
846
|
+
if (currentVersionInfo.crashRescue) {
|
|
847
|
+
this.report({ type: 'crashRescue' });
|
|
848
|
+
}
|
|
841
849
|
};
|
|
842
850
|
switchVersion = async (hash: string) => {
|
|
843
851
|
if (!this.assertDebug('switchVersion()')) {
|
package/src/type.ts
CHANGED
|
@@ -85,6 +85,12 @@ export type EventType =
|
|
|
85
85
|
| 'errorUpdate'
|
|
86
86
|
| 'markSuccess'
|
|
87
87
|
| 'errorMarkSuccess'
|
|
88
|
+
// 救砖回执:被 forceBoot / 崩溃时刻救援送进来的版本活到了 markSuccess。
|
|
89
|
+
// 原生在下载提交时往 hashInfo 写对应标记(§10.7 / §11.3),JS 在
|
|
90
|
+
// markSuccess 后各补报一条;老服务端对未知 type 400,report 本就是
|
|
91
|
+
// best-effort 静默丢弃,无害。
|
|
92
|
+
| 'forceBootRescue'
|
|
93
|
+
| 'crashRescue'
|
|
88
94
|
| 'reset'
|
|
89
95
|
| 'errorReset'
|
|
90
96
|
// Server reported bundleStatus: unknownBundle — the installed binary is not
|