Skip to content

Commit 4a0b119

Browse files
committed
fix(ios): verify livesync payload delivery on physical devices and stop swallowing upload errors
On physical iOS devices a full sync transfers the app as a single sync.zip over AFC (house_arrest) which the runtime's TKLiveSync extracts at next boot. When that delivery failed, nothing noticed: upload errors whose deviceId didn't exactly match the target device (including errors with no deviceId at all) were silently dropped, there was no post-transfer verification, and "Successfully synced" printed regardless — leaving the app running the stale JavaScript baked into the installed .app payload with no indication anywhere. - After transferring sync.zip, list the LiveSync directory on the device and confirm the zip actually landed; retry the transfer once, then fail the sync loudly (with a --clean remediation hint) instead of reporting success over stale code. Note: the listing targets the LiveSync ROOT — getDeviceProjectRootPath() returns .../LiveSync/app, one level below where the zip is uploaded. - uploadFilesCore now rethrows unattributed upload errors and logs errors attributed to other devices instead of ignoring them. - New optional IDeviceFileSystem.getDirectoryEntries(), implemented for physical iOS devices (AFC), backs the verification.
1 parent ee2b2f1 commit 4a0b119

3 files changed

Lines changed: 215 additions & 43 deletions

File tree

lib/common/definitions/mobile.d.ts

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -258,8 +258,7 @@ declare global {
258258
* Describes different options for filtering device logs.
259259
*/
260260
interface IDeviceLogOptions
261-
extends IDictionary<string | boolean>,
262-
Partial<IProjectDir> {
261+
extends IDictionary<string | boolean>, Partial<IProjectDir> {
263262
/**
264263
* Process id of the application on the device.
265264
*/
@@ -284,8 +283,7 @@ declare global {
284283
* Describes required methods for getting iOS Simulator's logs.
285284
*/
286285
interface IiOSSimulatorLogProvider
287-
extends NodeJS.EventEmitter,
288-
IShouldDispose {
286+
extends NodeJS.EventEmitter, IShouldDispose {
289287
/**
290288
* Starts the process for getting simulator logs and emits and DEVICE_LOG_EVENT_NAME event.
291289
* @param {string} deviceId The unique identifier of the device.
@@ -429,6 +427,16 @@ declare global {
429427

430428
interface IDeviceFileSystem {
431429
listFiles(devicePath: string, appIdentifier?: string): Promise<any>;
430+
/**
431+
* Returns the entries of a directory inside the application's
432+
* sandbox, or null when the directory cannot be read. Currently
433+
* implemented only for physical iOS devices (AFC), where it backs
434+
* the post-transfer livesync verification.
435+
*/
436+
getDirectoryEntries?(
437+
devicePath: string,
438+
appIdentifier: string,
439+
): Promise<string[] | null>;
432440
getFile(
433441
deviceFilePath: string,
434442
appIdentifier: string,
@@ -533,8 +541,7 @@ declare global {
533541
/**
534542
* Describes options that can be passed to devices service's initialization method.
535543
*/
536-
interface IDevicesServicesInitializationOptions
537-
extends Partial<IDeviceLookingOptions> {
544+
interface IDevicesServicesInitializationOptions extends Partial<IDeviceLookingOptions> {
538545
/**
539546
* If passed will start an emulator if necesasry.
540547
*/
@@ -1261,8 +1268,7 @@ declare global {
12611268
}
12621269

12631270
interface IDeviceLookingOptions
1264-
extends IHasEmulatorOption,
1265-
IHasDetectionInterval {
1271+
extends IHasEmulatorOption, IHasDetectionInterval {
12661272
shouldReturnImmediateResult: boolean;
12671273
platform: string;
12681274
fullDiscovery?: boolean;
@@ -1388,8 +1394,7 @@ declare global {
13881394
/**
13891395
* Describes information about application on device.
13901396
*/
1391-
interface IDeviceApplicationInformation
1392-
extends IDeviceApplicationInformationBase {
1397+
interface IDeviceApplicationInformation extends IDeviceApplicationInformationBase {
13931398
/**
13941399
* The framework of the project (Cordova or NativeScript).
13951400
*/

lib/common/mobile/ios/device/ios-device-file-system.ts

Lines changed: 54 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ export class IOSDeviceFileSystem implements Mobile.IDeviceFileSystem {
99
private device: Mobile.IDevice,
1010
private $logger: ILogger,
1111
private $iosDeviceOperations: IIOSDeviceOperations,
12-
private $fs: IFileSystem
12+
private $fs: IFileSystem,
1313
) {}
1414

1515
public async listFiles(
1616
devicePath: string,
17-
appIdentifier: string
17+
appIdentifier: string,
1818
): Promise<void> {
1919
if (!devicePath) {
2020
devicePath = ".";
@@ -31,10 +31,33 @@ export class IOSDeviceFileSystem implements Mobile.IDeviceFileSystem {
3131
this.$logger.info(children.join(EOL));
3232
}
3333

34+
public async getDirectoryEntries(
35+
devicePath: string,
36+
appIdentifier: string,
37+
): Promise<string[] | null> {
38+
try {
39+
const result = await this.$iosDeviceOperations.listDirectory([
40+
{
41+
deviceId: this.device.deviceInfo.identifier,
42+
path: devicePath,
43+
appId: appIdentifier,
44+
},
45+
]);
46+
const entries =
47+
result?.[this.device.deviceInfo.identifier]?.[0]?.response;
48+
return Array.isArray(entries) ? entries : null;
49+
} catch (err) {
50+
this.$logger.trace(
51+
`Unable to list directory '${devicePath}' for application ${appIdentifier}: ${err.message}`,
52+
);
53+
return null;
54+
}
55+
}
56+
3457
public async getFile(
3558
deviceFilePath: string,
3659
appIdentifier: string,
37-
outputFilePath?: string
60+
outputFilePath?: string,
3861
): Promise<void> {
3962
if (outputFilePath) {
4063
await this.$iosDeviceOperations.downloadFiles([
@@ -50,14 +73,14 @@ export class IOSDeviceFileSystem implements Mobile.IDeviceFileSystem {
5073

5174
const fileContent = await this.getFileContent(
5275
deviceFilePath,
53-
appIdentifier
76+
appIdentifier,
5477
);
5578
this.$logger.info(fileContent);
5679
}
5780

5881
public async getFileContent(
5982
deviceFilePath: string,
60-
appIdentifier: string
83+
appIdentifier: string,
6184
): Promise<string> {
6285
const result = await this.$iosDeviceOperations.readFiles([
6386
{
@@ -73,7 +96,7 @@ export class IOSDeviceFileSystem implements Mobile.IDeviceFileSystem {
7396
public async putFile(
7497
localFilePath: string,
7598
deviceFilePath: string,
76-
appIdentifier: string
99+
appIdentifier: string,
77100
): Promise<void> {
78101
await this.uploadFilesCore([
79102
{
@@ -86,7 +109,7 @@ export class IOSDeviceFileSystem implements Mobile.IDeviceFileSystem {
86109

87110
public async deleteFile(
88111
deviceFilePath: string,
89-
appIdentifier: string
112+
appIdentifier: string,
90113
): Promise<void> {
91114
await this.$iosDeviceOperations.deleteFiles(
92115
[
@@ -98,25 +121,25 @@ export class IOSDeviceFileSystem implements Mobile.IDeviceFileSystem {
98121
],
99122
(err: IOSDeviceLib.IDeviceError) => {
100123
this.$logger.trace(
101-
`Error while deleting file: ${deviceFilePath}: ${err.message} with code: ${err.code}`
124+
`Error while deleting file: ${deviceFilePath}: ${err.message} with code: ${err.code}`,
102125
);
103126

104127
if (err.code !== IOSDeviceFileSystem.AFC_DELETE_FILE_NOT_FOUND_ERROR) {
105128
this.$logger.warn(
106-
`Cannot delete file: ${deviceFilePath}. Reason: ${err.message}`
129+
`Cannot delete file: ${deviceFilePath}. Reason: ${err.message}`,
107130
);
108131
}
109-
}
132+
},
110133
);
111134
}
112135

113136
public async transferFiles(
114137
deviceAppData: Mobile.IDeviceAppData,
115-
localToDevicePaths: Mobile.ILocalToDevicePathData[]
138+
localToDevicePaths: Mobile.ILocalToDevicePathData[],
116139
): Promise<Mobile.ILocalToDevicePathData[]> {
117140
const filesToUpload: Mobile.ILocalToDevicePathData[] = _.filter(
118141
localToDevicePaths,
119-
(l) => this.$fs.getFsStats(l.getLocalPath()).isFile()
142+
(l) => this.$fs.getFsStats(l.getLocalPath()).isFile(),
120143
);
121144
const files: IOSDeviceLib.IFileData[] = filesToUpload.map((l) => ({
122145
source: l.getLocalPath(),
@@ -137,29 +160,43 @@ export class IOSDeviceFileSystem implements Mobile.IDeviceFileSystem {
137160
public async transferDirectory(
138161
deviceAppData: Mobile.IDeviceAppData,
139162
localToDevicePaths: Mobile.ILocalToDevicePathData[],
140-
projectFilesPath: string
163+
projectFilesPath: string,
141164
): Promise<Mobile.ILocalToDevicePathData[]> {
142165
await this.transferFiles(deviceAppData, localToDevicePaths);
143166
return localToDevicePaths;
144167
}
145168

146169
public async updateHashesOnDevice(
147170
hashes: IStringDictionary,
148-
appIdentifier: string
171+
appIdentifier: string,
149172
): Promise<void> {
150173
return;
151174
}
152175

153176
private async uploadFilesCore(
154-
filesToUpload: IOSDeviceLib.IUploadFilesData[]
177+
filesToUpload: IOSDeviceLib.IUploadFilesData[],
155178
): Promise<void> {
156179
await this.$iosDeviceOperations.uploadFiles(
157180
filesToUpload,
158181
(err: IOSDeviceLib.IDeviceError) => {
159-
if (err.deviceId === this.device.deviceInfo.identifier) {
182+
// Previously an error whose deviceId did not exactly match was
183+
// dropped on the floor — including errors with NO deviceId at
184+
// all (some ios-device-lib error paths don't attribute one).
185+
// That left "Successfully synced" printed over a failed
186+
// transfer and the app silently running stale JavaScript.
187+
// Rethrow unless the error is positively attributed to a
188+
// DIFFERENT device; surface even those at warn level so a
189+
// failed upload is never invisible.
190+
if (
191+
!err.deviceId ||
192+
err.deviceId === this.device.deviceInfo.identifier
193+
) {
160194
throw err;
161195
}
162-
}
196+
this.$logger.warn(
197+
`File upload error reported for another device (${err.deviceId}): ${err.message}`,
198+
);
199+
},
163200
);
164201
}
165202
}

0 commit comments

Comments
 (0)