Skip to content

Commit 3f0d21c

Browse files
committed
feat: Add cleanup lock for "ui5 serve"
1 parent f73ea9d commit 3f0d21c

3 files changed

Lines changed: 78 additions & 32 deletions

File tree

packages/cli/lib/cli/commands/serve.js

Lines changed: 70 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import path from "node:path";
22
import os from "node:os";
3+
import fs from "node:fs/promises";
4+
import {promisify} from "node:util";
35
import chalk from "chalk";
46
import baseMiddleware from "../middlewares/base.js";
7+
import {getUi5DataDir} from "../../framework/utils.js";
8+
import lockfile from "lockfile";
59
import {getLogger} from "@ui5/logger";
610
const log = getLogger("cli:commands:serve");
711

@@ -189,42 +193,76 @@ serve.handler = async function(argv) {
189193
reject(err);
190194
});
191195

192-
const protocol = h2 ? "https" : "http";
193-
let browserUrl = protocol + "://localhost:" + actualPort;
194-
if (argv.acceptRemoteConnections) {
195-
process.stderr.write("\n");
196-
process.stderr.write(chalk.bold("⚠️ This server is accepting connections from all hosts on your network"));
197-
process.stderr.write("\n");
198-
process.stderr.write(chalk.dim.underline("Please Note:"));
199-
process.stderr.write("\n");
200-
process.stderr.write(chalk.bold.dim(
201-
"* This server is intended for development purposes only. Do not use it in production."));
202-
process.stderr.write("\n");
203-
process.stderr.write(chalk.dim(
204-
"* Vulnerable (custom-)middleware can pose a threat to your system when exposed to the network"));
205-
process.stderr.write("\n");
206-
process.stderr.write(chalk.dim(
207-
"* The use of proxy-middleware with preconfigured credentials might enable unauthorized access " +
208-
"to a target system for third parties on your network"));
209-
process.stderr.write("\n\n");
210-
}
211-
process.stdout.write("Server started");
212-
process.stdout.write("\n");
213-
process.stdout.write("URL: " + browserUrl);
214-
process.stdout.write("\n");
196+
// Acquire a port-specific server lock so that 'ui5 cache clean' cannot delete
197+
// framework files that the server reads on every HTTP request. Multiple concurrent
198+
// servers each hold their own lock; cleanCache sees any active lock and refuses.
199+
const ui5DataDir = (await getUi5DataDir({cwd: process.cwd()})) ??
200+
path.join(os.homedir(), ".ui5");
201+
const lockDir = path.join(ui5DataDir, "framework", "locks");
202+
const lockPath = path.join(lockDir, `server-${actualPort}.lock`);
203+
await fs.mkdir(lockDir, {recursive: true});
204+
const lockFn = promisify(lockfile.lock);
205+
const unlockFn = promisify(lockfile.unlock);
206+
await lockFn(lockPath, {stale: 60000});
207+
let lockReleased = false;
208+
const releaseServerLock = async () => {
209+
if (lockReleased) return;
210+
lockReleased = true;
211+
await unlockFn(lockPath).catch(() => {});
212+
};
213+
// Signal handlers must be synchronous — Node does not await async handlers before exit.
214+
const onSignal = () => {
215+
if (!lockReleased) {
216+
lockReleased = true;
217+
lockfile.unlockSync(lockPath);
218+
}
219+
process.exit(0);
220+
};
221+
process.once("SIGINT", onSignal);
222+
process.once("SIGTERM", onSignal);
223+
224+
try {
225+
const protocol = h2 ? "https" : "http";
226+
let browserUrl = protocol + "://localhost:" + actualPort;
227+
if (argv.acceptRemoteConnections) {
228+
process.stderr.write("\n");
229+
process.stderr.write(chalk.bold("⚠️ This server is accepting connections from all hosts on your network"));
230+
process.stderr.write("\n");
231+
process.stderr.write(chalk.dim.underline("Please Note:"));
232+
process.stderr.write("\n");
233+
process.stderr.write(chalk.bold.dim(
234+
"* This server is intended for development purposes only. Do not use it in production."));
235+
process.stderr.write("\n");
236+
process.stderr.write(chalk.dim(
237+
"* Vulnerable (custom-)middleware can pose a threat to your system when exposed to the network"));
238+
process.stderr.write("\n");
239+
process.stderr.write(chalk.dim(
240+
"* The use of proxy-middleware with preconfigured credentials might enable unauthorized access " +
241+
"to a target system for third parties on your network"));
242+
process.stderr.write("\n\n");
243+
}
244+
process.stdout.write("Server started");
245+
process.stdout.write("\n");
246+
process.stdout.write("URL: " + browserUrl);
247+
process.stdout.write("\n");
215248

216-
if (argv.open !== undefined) {
217-
if (typeof argv.open === "string") {
218-
let relPath = argv.open || "/";
219-
if (!relPath.startsWith("/")) {
220-
relPath = "/" + relPath;
249+
if (argv.open !== undefined) {
250+
if (typeof argv.open === "string") {
251+
let relPath = argv.open || "/";
252+
if (!relPath.startsWith("/")) {
253+
relPath = "/" + relPath;
254+
}
255+
browserUrl += relPath;
221256
}
222-
browserUrl += relPath;
257+
const {default: open} = await import("open");
258+
open(browserUrl);
223259
}
224-
const {default: open} = await import("open");
225-
open(browserUrl);
260+
await pOnError; // Await errors that should bubble into the yargs handler
261+
} finally {
262+
process.off("SIGINT", onSignal);
263+
process.off("SIGTERM", onSignal);
264+
await releaseServerLock();
226265
}
227-
await pOnError; // Await errors that should bubble into the yargs handler
228266
};
229267

230268
export default serve;

packages/cli/test/lib/cli/commands/serve.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,13 @@ test.beforeEach(async (t) => {
8181
"@ui5/server": t.context.server,
8282
"@ui5/server/internal/sslUtil": t.context.sslUtil,
8383
"@ui5/project/graph": t.context.graph,
84+
"../../../../lib/framework/utils.js": {
85+
getUi5DataDir: sinon.stub().resolves(undefined)
86+
},
87+
"lockfile": {
88+
lock: sinon.stub().yieldsAsync(),
89+
unlock: sinon.stub().yieldsAsync()
90+
},
8491
"open": t.context.open
8592
});
8693
});

packages/project/lib/ui5Framework/_frameworkPaths.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export const LOCK_STALE_MS = 60000;
1515
// names are replaced with dashes by AbstractInstaller#_sanitizeFileName):
1616
// cache-cleanup.lock — held by ui5 cache clean for the full deletion
1717
// package-{pkg}@{ver}.lock — held by both installers during package extraction
18+
// (callers may add their own lock files to signal activity to cache cleanup)
1819
export const CLEANUP_LOCK_NAME = "cache-cleanup.lock";
1920

2021
/**

0 commit comments

Comments
 (0)