@camstack/server 1.1.38 → 1.1.39
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.
|
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.resolveHubClosurePackageDir = resolveHubClosurePackageDir;
|
|
37
37
|
exports.resolveOverlayPackageDir = resolveOverlayPackageDir;
|
|
38
38
|
exports.computeDistBuildId = computeDistBuildId;
|
|
39
|
+
exports.swapDist = swapDist;
|
|
39
40
|
exports.syncFrameworkDist = syncFrameworkDist;
|
|
40
41
|
/**
|
|
41
42
|
* Framework live-sync — make a `camstack deploy` of a framework package
|
|
@@ -202,17 +203,27 @@ function computeDistBuildId(distDir) {
|
|
|
202
203
|
// ---------------------------------------------------------------------------
|
|
203
204
|
// Atomic dist swap (rename-aside)
|
|
204
205
|
// ---------------------------------------------------------------------------
|
|
206
|
+
function isCrossDeviceError(err) {
|
|
207
|
+
return err instanceof Error && err.code === 'EXDEV';
|
|
208
|
+
}
|
|
205
209
|
/**
|
|
206
|
-
* Replace `<targetPkgDir>/dist` with a copy of `sourceDistDir
|
|
210
|
+
* Replace `<targetPkgDir>/dist` with a copy of `sourceDistDir`.
|
|
211
|
+
*
|
|
212
|
+
* 1. COPY `sourceDistDir` → a staging dir INSIDE the target dir. The live
|
|
213
|
+
* `dist/` is untouched, so a mid-copy failure cannot corrupt it.
|
|
214
|
+
* 2. Evict the current `dist/` aside (rename), then rename the staging dir into
|
|
215
|
+
* place. Both renames are metadata ops WITHIN the target fs (atomic). If the
|
|
216
|
+
* final swap fails, the previous `dist/` is restored.
|
|
207
217
|
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
218
|
+
* OVERLAYFS FALLBACK: the hub-closure target (`/opt/.../@camstack/system`) lives
|
|
219
|
+
* on the container's overlay rootfs. A `dist/` materialized from a LOWER image
|
|
220
|
+
* layer cannot be `rename()`d (overlayfs returns `EXDEV: cross-device link`),
|
|
221
|
+
* so the atomic aside/swap is impossible there. On EXDEV we fall back to a
|
|
222
|
+
* copy-aside restore point + in-place replace. This opens a brief non-atomic
|
|
223
|
+
* window, but it is safe here: the running process already holds the old
|
|
224
|
+
* modules in memory (nothing reads `dist/` until the scheduled restart), and
|
|
225
|
+
* the copy-aside is kept until the replace completes so a failure is
|
|
226
|
+
* recoverable.
|
|
216
227
|
*/
|
|
217
228
|
async function swapDist(sourceDistDir, targetPkgDir) {
|
|
218
229
|
const targetDist = path.join(targetPkgDir, 'dist');
|
|
@@ -227,24 +238,63 @@ async function swapDist(sourceDistDir, targetPkgDir) {
|
|
|
227
238
|
await fs.promises.rm(incoming, { recursive: true, force: true }).catch(() => undefined);
|
|
228
239
|
throw err;
|
|
229
240
|
}
|
|
230
|
-
// Step 2 — atomic swap within the target fs.
|
|
231
241
|
const hadDist = fs.existsSync(targetDist);
|
|
242
|
+
// How the previous dist was preserved: 'rename' (moved aside, same fs),
|
|
243
|
+
// 'copy' (overlayfs EXDEV — copied aside then removed in place), or 'none'.
|
|
244
|
+
let asideKind = 'none';
|
|
245
|
+
const restorePreviousDist = async () => {
|
|
246
|
+
if (asideKind === 'none' || fs.existsSync(targetDist))
|
|
247
|
+
return;
|
|
248
|
+
if (asideKind === 'rename') {
|
|
249
|
+
await fs.promises.rename(aside, targetDist).catch(() => undefined);
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
await fs.promises
|
|
253
|
+
.cp(aside, targetDist, { recursive: true, force: true })
|
|
254
|
+
.catch(() => undefined);
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
// Step 2 — evict the current dist. Prefer the atomic rename; fall back to
|
|
258
|
+
// copy-aside + in-place remove on overlayfs EXDEV.
|
|
232
259
|
if (hadDist) {
|
|
233
|
-
|
|
260
|
+
try {
|
|
261
|
+
await fs.promises.rename(targetDist, aside);
|
|
262
|
+
asideKind = 'rename';
|
|
263
|
+
}
|
|
264
|
+
catch (err) {
|
|
265
|
+
if (!isCrossDeviceError(err)) {
|
|
266
|
+
await fs.promises.rm(incoming, { recursive: true, force: true }).catch(() => undefined);
|
|
267
|
+
throw err;
|
|
268
|
+
}
|
|
269
|
+
await fs.promises.cp(targetDist, aside, { recursive: true, force: true });
|
|
270
|
+
await fs.promises.rm(targetDist, { recursive: true, force: true });
|
|
271
|
+
asideKind = 'copy';
|
|
272
|
+
}
|
|
234
273
|
}
|
|
274
|
+
// Step 3 — move the incoming dist into place; fall back to copy on EXDEV.
|
|
235
275
|
try {
|
|
236
276
|
await fs.promises.rename(incoming, targetDist);
|
|
237
277
|
}
|
|
238
|
-
catch (
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
278
|
+
catch (err) {
|
|
279
|
+
if (isCrossDeviceError(err)) {
|
|
280
|
+
try {
|
|
281
|
+
await fs.promises.cp(incoming, targetDist, { recursive: true, force: true });
|
|
282
|
+
await fs.promises.rm(incoming, { recursive: true, force: true }).catch(() => undefined);
|
|
283
|
+
}
|
|
284
|
+
catch (copyErr) {
|
|
285
|
+
await restorePreviousDist();
|
|
286
|
+
await fs.promises.rm(incoming, { recursive: true, force: true }).catch(() => undefined);
|
|
287
|
+
throw copyErr;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
await restorePreviousDist();
|
|
292
|
+
await fs.promises.rm(incoming, { recursive: true, force: true }).catch(() => undefined);
|
|
293
|
+
throw err;
|
|
242
294
|
}
|
|
243
|
-
await fs.promises.rm(incoming, { recursive: true, force: true }).catch(() => undefined);
|
|
244
|
-
throw swapErr;
|
|
245
295
|
}
|
|
246
|
-
// Step
|
|
247
|
-
if (
|
|
296
|
+
// Step 4 — best-effort cleanup of the evicted copy.
|
|
297
|
+
if (asideKind !== 'none') {
|
|
248
298
|
await fs.promises.rm(aside, { recursive: true, force: true }).catch(() => undefined);
|
|
249
299
|
}
|
|
250
300
|
}
|