Skip to content

Commit 889af27

Browse files
committed
fix: improve failure discovery
1 parent e478e6b commit 889af27

1 file changed

Lines changed: 114 additions & 34 deletions

File tree

lib/services/livesync/ios-livesync-service.ts

Lines changed: 114 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -82,51 +82,131 @@ export class IOSLiveSyncService
8282
deviceProjectRootPath,
8383
},
8484
]);
85-
await transferSyncZip();
8685

87-
// Verify the zip actually landed in the app sandbox. The AFC
88-
// transfer has been observed to fail silently on some runs (the
89-
// runtime then boots the stale JavaScript baked into the installed
90-
// .app payload, with no indication anywhere that the sync was
91-
// lost). One retry covers transient AFC hiccups; if delivery still
92-
// cannot be confirmed, fail the sync loudly instead of printing
93-
// "Successfully synced" over stale code — the run-controller
94-
// surfaces the error and the user can re-run (or use --clean).
95-
const verifySyncZipDelivered = async (): Promise<boolean> => {
96-
if (!device.fileSystem.getDirectoryEntries) {
97-
return true; // no verification support — assume delivered
98-
}
99-
// NOTE: deviceProjectRootPath is `.../LiveSync/app` (the extracted
100-
// app folder); the zip is uploaded one level up, at the LiveSync
101-
// root — list THAT directory.
102-
const entries = await device.fileSystem.getDirectoryEntries(
86+
// ── Fail-closed delivery verification ──────────────────────────
87+
//
88+
// The AFC transfer has been observed to fail without surfacing an
89+
// error, leaving the app to boot the stale JavaScript baked into
90+
// the installed .app payload with no indication anywhere that the
91+
// sync was lost. After the transfer we therefore confirm the zip
92+
// is actually present in the app sandbox; one retry covers
93+
// transient AFC hiccups, and an unconfirmed delivery fails the
94+
// sync loudly instead of printing "Successfully synced" over
95+
// stale code (the run-controller surfaces the error; --clean
96+
// reinstalls the full package).
97+
//
98+
// Presence alone is NOT sufficient evidence: a previous run that
99+
// transferred the zip but aborted before the app restarted leaves
100+
// a LEFTOVER sync.zip behind (the runtime only consumes it at
101+
// boot), which would satisfy the check even when THIS upload
102+
// failed. So any pre-existing zip is deleted up front — after
103+
// that, post-transfer presence can only be produced by this run's
104+
// upload.
105+
//
106+
// NOTE: deviceProjectRootPath is `.../LiveSync/app` (the extracted
107+
// app folder); the zip is uploaded one level up, at the LiveSync
108+
// root — the listing targets THAT directory.
109+
//
110+
// Escape hatch: NS_SKIP_IOS_SYNC_VERIFICATION=1 disables the
111+
// whole verification for exotic setups where directory listing
112+
// misbehaves but uploads are known-good.
113+
const syncZipDevicePath = deviceAppData.deviceSyncZipPath;
114+
const verificationSupported =
115+
!!device.fileSystem.getDirectoryEntries &&
116+
process.env.NS_SKIP_IOS_SYNC_VERIFICATION !== "1";
117+
const listLiveSyncRoot = (): Promise<string[] | null> =>
118+
device.fileSystem.getDirectoryEntries(
103119
LiveSyncPaths.IOS_DEVICE_PROJECT_ROOT_PATH,
104120
deviceAppData.appIdentifier,
105121
);
122+
const containsSyncZip = (entries: string[]): boolean =>
123+
entries.some(
124+
(entry) => entry === syncZipDevicePath || entry.endsWith("/sync.zip"),
125+
);
126+
// "delivered" / "missing" are definitive listings; "unknown" means
127+
// the listing itself could not be read (after one retry).
128+
const checkDelivery = async (): Promise<
129+
"delivered" | "missing" | "unknown"
130+
> => {
131+
let entries = await listLiveSyncRoot();
106132
if (entries === null) {
107-
// Could not read the directory at all — don't fail the run on
108-
// the verification mechanism itself, but leave a trace.
109-
this.$logger.trace(
110-
"Unable to verify sync.zip delivery (directory listing unavailable); continuing.",
111-
);
112-
return true;
133+
entries = await listLiveSyncRoot();
134+
}
135+
if (entries === null) {
136+
return "unknown";
113137
}
114-
return entries.some((entry) => entry.endsWith("sync.zip"));
138+
return containsSyncZip(entries) ? "delivered" : "missing";
115139
};
116140

117-
if (!(await verifySyncZipDelivered())) {
118-
this.$logger.warn(
119-
"sync.zip was not found on the device after transfer — retrying once...",
141+
let preListingAvailable = false;
142+
let leftoverZipPresent = false;
143+
if (verificationSupported) {
144+
// Clear any leftover zip so the post-transfer check attributes
145+
// presence to this run. Best-effort: AFC "file not found" is
146+
// tolerated inside deleteFile.
147+
await device.fileSystem.deleteFile(
148+
syncZipDevicePath,
149+
deviceAppData.appIdentifier,
120150
);
121-
await transferSyncZip();
122-
if (!(await verifySyncZipDelivered())) {
123-
throw new Error(
124-
`Unable to deliver the application payload (sync.zip) to device ${device.deviceInfo.identifier}. ` +
125-
`The app would run stale JavaScript without it. ` +
126-
`Re-run the command, or use a clean rebuild (--clean) to reinstall the full application package.`,
151+
const preEntries = await listLiveSyncRoot();
152+
preListingAvailable = Array.isArray(preEntries);
153+
leftoverZipPresent = preListingAvailable && containsSyncZip(preEntries);
154+
}
155+
156+
await transferSyncZip();
157+
158+
if (verificationSupported) {
159+
if (leftoverZipPresent) {
160+
// The pre-transfer delete did not take effect, so presence
161+
// can no longer be attributed to this run. Most likely the
162+
// upload succeeded too, but say so explicitly rather than
163+
// claim verification.
164+
this.$logger.warn(
165+
"A leftover sync.zip from a previous run could not be removed — delivery verification for this sync is inconclusive. " +
166+
"If the app runs stale code, re-run the command or use a clean rebuild (--clean).",
127167
);
168+
} else {
169+
let state = await checkDelivery();
170+
if (state === "missing") {
171+
this.$logger.warn(
172+
"sync.zip was not found on the device after transfer — retrying once...",
173+
);
174+
await transferSyncZip();
175+
state = await checkDelivery();
176+
if (state === "delivered") {
177+
this.$logger.info("sync.zip delivered on retry.");
178+
}
179+
}
180+
if (state === "missing") {
181+
throw new Error(
182+
`Unable to deliver the application payload (sync.zip) to device ${device.deviceInfo.identifier}. ` +
183+
`The app would run stale JavaScript without it. ` +
184+
`Re-run the command, or use a clean rebuild (--clean) to reinstall the full application package.`,
185+
);
186+
}
187+
if (state === "unknown") {
188+
if (preListingAvailable) {
189+
// The listing worked moments before the upload and
190+
// broke right after it — the AFC session is
191+
// misbehaving at exactly the point where the upload
192+
// itself is suspect. Fail closed.
193+
throw new Error(
194+
`Unable to confirm delivery of the application payload (sync.zip) to device ${device.deviceInfo.identifier}: ` +
195+
`the device directory listing failed right after the transfer. ` +
196+
`Re-run the command, or use a clean rebuild (--clean) to reinstall the full application package. ` +
197+
`(Set NS_SKIP_IOS_SYNC_VERIFICATION=1 to bypass delivery verification.)`,
198+
);
199+
}
200+
// Listing was unavailable both before and after the
201+
// transfer — verification is unsupported for this
202+
// device/session. This is the single fail-open path,
203+
// and it is loud rather than silent.
204+
this.$logger.warn(
205+
"Could not verify sync.zip delivery (device directory listing unavailable). " +
206+
"If the transfer failed, the app will run stale JavaScript — re-run the command or use a clean rebuild (--clean).",
207+
);
208+
}
128209
}
129-
this.$logger.info("sync.zip delivered on retry.");
130210
}
131211

132212
await deviceAppData.device.applicationManager.setTransferredAppFiles(

0 commit comments

Comments
 (0)