cli/server: harden run startup for port conflicts and entrypoint loading
This commit is contained in:
@@ -64,41 +64,60 @@ export async function runCommand(opts: RunOptions): Promise<void> {
|
|||||||
await importServerEntry();
|
await importServerEntry();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatError(err: unknown): string {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
if (err.message && err.message.trim().length > 0) return err.message;
|
||||||
|
return err.name;
|
||||||
|
}
|
||||||
|
if (typeof err === "string") return err;
|
||||||
|
try {
|
||||||
|
return JSON.stringify(err);
|
||||||
|
} catch {
|
||||||
|
return String(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isModuleNotFoundError(err: unknown): boolean {
|
||||||
|
if (!(err instanceof Error)) return false;
|
||||||
|
const code = (err as { code?: unknown }).code;
|
||||||
|
if (code === "ERR_MODULE_NOT_FOUND") return true;
|
||||||
|
return err.message.includes("Cannot find module");
|
||||||
|
}
|
||||||
|
|
||||||
async function importServerEntry(): Promise<void> {
|
async function importServerEntry(): Promise<void> {
|
||||||
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
|
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
|
||||||
const fileCandidates = [
|
const fileCandidates = [
|
||||||
path.resolve(projectRoot, "server/dist/index.js"),
|
|
||||||
path.resolve(projectRoot, "server/src/index.ts"),
|
path.resolve(projectRoot, "server/src/index.ts"),
|
||||||
|
path.resolve(projectRoot, "server/dist/index.js"),
|
||||||
];
|
];
|
||||||
|
const existingFileCandidates = fileCandidates.filter((filePath) => fs.existsSync(filePath));
|
||||||
|
if (existingFileCandidates.length > 0) {
|
||||||
|
for (const filePath of existingFileCandidates) {
|
||||||
|
try {
|
||||||
|
await import(pathToFileURL(filePath).href);
|
||||||
|
return;
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(`Failed to start Paperclip server from ${filePath}: ${formatError(err)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const specifierCandidates: string[] = [
|
const specifierCandidates: string[] = ["@paperclipai/server/dist/index.js", "@paperclipai/server/src/index.ts"];
|
||||||
"@paperclipai/server/dist/index.js",
|
const missingErrors: string[] = [];
|
||||||
"@paperclipai/server/src/index.ts",
|
|
||||||
];
|
|
||||||
|
|
||||||
const importErrors: string[] = [];
|
|
||||||
|
|
||||||
for (const specifier of specifierCandidates) {
|
for (const specifier of specifierCandidates) {
|
||||||
try {
|
try {
|
||||||
await import(specifier);
|
await import(specifier);
|
||||||
return;
|
return;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
importErrors.push(`${specifier}: ${err instanceof Error ? err.message : String(err)}`);
|
if (isModuleNotFoundError(err)) {
|
||||||
}
|
missingErrors.push(`${specifier}: ${formatError(err)}`);
|
||||||
}
|
continue;
|
||||||
|
}
|
||||||
for (const filePath of fileCandidates) {
|
throw new Error(`Failed to start Paperclip server from ${specifier}: ${formatError(err)}`);
|
||||||
if (!fs.existsSync(filePath)) continue;
|
|
||||||
try {
|
|
||||||
await import(pathToFileURL(filePath).href);
|
|
||||||
return;
|
|
||||||
} catch (err) {
|
|
||||||
importErrors.push(`${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Could not start Paperclip server entrypoint. Tried: ${[...specifierCandidates, ...fileCandidates].join(", ")}\n` +
|
`Could not locate a Paperclip server entrypoint. Tried: ${[...fileCandidates, ...specifierCandidates].join(", ")}\n${missingErrors.join("\n")}`,
|
||||||
importErrors.join("\n"),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -225,27 +225,14 @@ if (config.databaseUrl) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const dataDir = resolve(config.embeddedPostgresDataDir);
|
const dataDir = resolve(config.embeddedPostgresDataDir);
|
||||||
const port = config.embeddedPostgresPort;
|
const configuredPort = config.embeddedPostgresPort;
|
||||||
|
let port = configuredPort;
|
||||||
|
|
||||||
if (config.databaseMode === "postgres") {
|
if (config.databaseMode === "postgres") {
|
||||||
logger.warn("Database mode is postgres but no connection string was set; falling back to embedded PostgreSQL");
|
logger.warn("Database mode is postgres but no connection string was set; falling back to embedded PostgreSQL");
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`No DATABASE_URL set — using embedded PostgreSQL (${dataDir}) on port ${port}`);
|
|
||||||
embeddedPostgres = new EmbeddedPostgres({
|
|
||||||
databaseDir: dataDir,
|
|
||||||
user: "paperclip",
|
|
||||||
password: "paperclip",
|
|
||||||
port,
|
|
||||||
persistent: true,
|
|
||||||
});
|
|
||||||
const clusterVersionFile = resolve(dataDir, "PG_VERSION");
|
const clusterVersionFile = resolve(dataDir, "PG_VERSION");
|
||||||
if (!existsSync(clusterVersionFile)) {
|
|
||||||
await embeddedPostgres.initialise();
|
|
||||||
} else {
|
|
||||||
logger.info(`Embedded PostgreSQL cluster already exists (${clusterVersionFile}); skipping init`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const postmasterPidFile = resolve(dataDir, "postmaster.pid");
|
const postmasterPidFile = resolve(dataDir, "postmaster.pid");
|
||||||
const isPidRunning = (pid: number): boolean => {
|
const isPidRunning = (pid: number): boolean => {
|
||||||
try {
|
try {
|
||||||
@@ -271,8 +258,31 @@ if (config.databaseUrl) {
|
|||||||
|
|
||||||
const runningPid = getRunningPid();
|
const runningPid = getRunningPid();
|
||||||
if (runningPid) {
|
if (runningPid) {
|
||||||
logger.warn({ pid: runningPid }, "Embedded PostgreSQL already running; reusing existing process");
|
logger.warn({ pid: runningPid, port }, "Embedded PostgreSQL already running; reusing existing process");
|
||||||
} else {
|
} else {
|
||||||
|
const detectedPort = await detectPort(configuredPort);
|
||||||
|
if (detectedPort !== configuredPort) {
|
||||||
|
logger.warn(
|
||||||
|
{ requestedPort: configuredPort, selectedPort: detectedPort },
|
||||||
|
"Embedded PostgreSQL port is in use; using next free port",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
port = detectedPort;
|
||||||
|
logger.info(`No DATABASE_URL set — using embedded PostgreSQL (${dataDir}) on port ${port}`);
|
||||||
|
embeddedPostgres = new EmbeddedPostgres({
|
||||||
|
databaseDir: dataDir,
|
||||||
|
user: "paperclip",
|
||||||
|
password: "paperclip",
|
||||||
|
port,
|
||||||
|
persistent: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existsSync(clusterVersionFile)) {
|
||||||
|
await embeddedPostgres.initialise();
|
||||||
|
} else {
|
||||||
|
logger.info(`Embedded PostgreSQL cluster already exists (${clusterVersionFile}); skipping init`);
|
||||||
|
}
|
||||||
|
|
||||||
if (existsSync(postmasterPidFile)) {
|
if (existsSync(postmasterPidFile)) {
|
||||||
logger.warn("Removing stale embedded PostgreSQL lock file");
|
logger.warn("Removing stale embedded PostgreSQL lock file");
|
||||||
rmSync(postmasterPidFile, { force: true });
|
rmSync(postmasterPidFile, { force: true });
|
||||||
|
|||||||
Reference in New Issue
Block a user