|
1 | 1 | import path from "node:path"; |
2 | 2 | import os from "node:os"; |
| 3 | +import fs from "node:fs/promises"; |
| 4 | +import {promisify} from "node:util"; |
3 | 5 | import chalk from "chalk"; |
4 | 6 | import baseMiddleware from "../middlewares/base.js"; |
| 7 | +import {getUi5DataDir} from "../../framework/utils.js"; |
| 8 | +import lockfile from "lockfile"; |
5 | 9 | import {getLogger} from "@ui5/logger"; |
6 | 10 | const log = getLogger("cli:commands:serve"); |
7 | 11 |
|
@@ -189,42 +193,76 @@ serve.handler = async function(argv) { |
189 | 193 | reject(err); |
190 | 194 | }); |
191 | 195 |
|
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"); |
215 | 248 |
|
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; |
221 | 256 | } |
222 | | - browserUrl += relPath; |
| 257 | + const {default: open} = await import("open"); |
| 258 | + open(browserUrl); |
223 | 259 | } |
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(); |
226 | 265 | } |
227 | | - await pOnError; // Await errors that should bubble into the yargs handler |
228 | 266 | }; |
229 | 267 |
|
230 | 268 | export default serve; |
0 commit comments